v0.24.0
2026-05-18
Top-Level Nav
- [x] Replace current purple cat logo in upper left with docs/design/cat icon 2.png
- [x] Only show the top-level nav when hovering over the cat icon's container (or the nav itself, when open); otherwise it is hidden.
- [x] Change the height of the purple cat container to match the height of the PlayTopBar (even when not on /play routes). Scale the cat icon so it fits comfortably in this height, taking up about 80% of the height. The container should be square when collapsed, and match the width of the nav when expanded.
Mobile
- [x] The "End Turn" button on mobile is underneath the mobile-only bottom bar. Move it up by the height of said bar (mobile only).
Initiative
- [x] Show the mini-initiative banner in ChatLog even when initiative is not active. In this mode, the only UI element is a button to roll initiative.
- [x] Add a sub-button to the Mini-Initiative "next turn" button with a vertical ... icon. This is a menu with the following options:
- End Initiative
- Go to Initiative Tracker
- [x] Make the Mini-Initiative banner background color a shade between the ChatLog background and the ChatLog header, to make it distinct from both.
Actions
- [x] Add a function to "token" called changeTokenArtwork(id:int). This has the effect of assigning source image with id supplied as the index of the token's artworks array to be the source image for the token's token artwork, with previously-saved or default tokenization settings. Example:
token.changeTokenArtwork(0)would set the token artwork to be the first source image. Note: this is not available on "target", only "token". - [x] Add a function to "token" called autoTokenizeArtwork(). This runs AI tokenization on the current token art. When done, it should update the token on the map.
- [x] Add a function to "token" called changePortraitArtwork(id:int). This has the effect of assigning source image with id supplied as the index of the token's artworks array to be the portrait image for the token. Example:
token.changePortraitArtwork(0)would set the portrait artwork to be the first source image. - [x] Add a property to token called currentTokenArtId, which is the index of the token's artworks array which is currently being used as the token art.
- [x] Add a property to token called currentPortraitArtId, which is the index of the token's artworks array which is currently being used as the portrait art.
Sidebars
- [x] Add resize handles to the left and right sidebar, allowing the user to resize them by dragging. Persist the sizes in local storage so they are remembered across sessions. When the user size isn't found, default to current width (20rem). Ensure the position of side-pinned elements like CurrentTokenBar are updated correctly on resize.
Character Context
These changes are specific to the Character Context (PlayModeView component).
- [x] This UI is now shown by default when a token is impersonated, and that token has a linked Character.
- [x] The character's portrait art (if present) is used as the background image, matching width to the container, mounted to the top of the container (sticky), with aspect ratio intact.
- [x] Use an algorithm or very fast in-browser model to analyze the portrait and determine its primary color, then assign that as the background color of the UI.
- [x] Add a padding-top of the same size as the container width, so most of the portrait will show by default before scrolling.
- [x] The attack buttons should execute their associated actions when clicked. Right now nothing happens; this is presumably a bug.
- [x] Instead of a badge for each ability score, simply render another line of text to show the bonus. Such as: "20 \n (+5)". When these + bonuses are clicked, roll 1d20 + that number. Indicate they are clickable.
- [x] Next to each power, show a caret to expand the power's actions inline. Render the actions as buttons (which fire off the action when clicked), and show an (i) icon next to each action which opens a popover with the full action description and any relevant metadata (e.g. for a spell, show the range, components, duration, etc). This saves the user from having to open the full Character Sheet just to see the details of an action.
- [x] Show the linked token's bars above the name/class/level section, with the same styling as CurrentTokenBar.
- [x] After the Ability Scores section, add a new section showing three values left-to-right: HP, AC, and Speed.
- [x] After the current last section, add a section for Vision & Lighting, using the same UI approach as the Vision & Lighting tab in the Edit Token modal.
- [x] After the Vision & Lighting section, add a section for Conditions, using the same UI approach as the Conditions tab in the Edit Token modal (except use two column layout for compactness).
- [x] After the Conditions section, add a section for Auras, using the same UI approach as the Auras tab in the Edit Token modal.
- [x] Do not show CurrentTokenBar for impersonated tokens anymore. (Keep showing it for Selected Tokens)
- [x] In Mobile, in the Me tab, Play sub-tab, show the Character Context UI instead of the current one.
- [x] Move the End Turn button from CurrentTokenBar (if that's where it is) to the map itself, so it shows in the lower left when you're impersonating a token, and it's that token's turn in initiative.
- [x] Next to 'edit this character' add another link button: 'edit this token'. This opens the Edit Token modal for the impersonated token.
- [x] Move the x, ..., focus, and zoom buttons from CurrentTokenBar into the Character Context, although they're pinned to the right edge of the name/class/level section instead of the portrait.
- In ..., remove View Character Sheet, Open Character Page, and Edit Token as those are now available in the Character Context itself.
- [x] In the Character Context, move the x, ..., focus, and zoom buttons so they are vertically-stacked along the right edge of the container.
- [x] In the Character Context, add tooltips to the x, focus, and zoom buttons to indicate their function.
- x button: "Stop Impersonating"
- ... button: "More Options"
- focus button: "Focus Camera on Token"
- zoom button: "Zoom Artwork"
- [x] In the Character Context on non-mobile, add the View / Build strip from the Mobile "Me" tab to the top of the Character Context (fixed position).
- View tab: shows the character sheet view as described above
- Build tab: shows the full character sheet edit UI. This should be simple, as it's already implemented in the Mobile "Me" tab. If not, make this tab a "coming soon" UI.
- Space to the right of the tabs: new home for x, ..., focus, and zoom
- [x] Bug: the tooltips for x, ..., focus, and zoom are underneath the context itself due to z-index
- [x] Change the Conditions section as follows:
- Only show conditions that are actually affecting the token.
- Show current conditions as a vertical list, with a trash icon to remove the condition (similar to the Auras section).
- Add an "Add Condition" button which opens a popover with a list of conditions to add.
- [x] Make the portrait image a regular image element instead of a background image, still matching the container width. Remove any padding-top that was used to ensure this image's visibility, as it's a block element now.
- [x] Make the gradient background fixed.
- [x] The x, ..., focus, and zoom buttons should be horizontally arranged in the new location
- [x] Bug: the special UI that shows when multiple tokens are selected doesn't appear when Character Context is open (or it's behind it). When the left panel is already open, the Multi-Select UI should be positioned to the right of the left panel.
- [x] Add token actions to the Actions section of the Character Context. These will include the attack actions from the character, which are already in this section, so remove those first.
Impersonated Token Context
A UI similar to the Character Context, but for tokens that are impersonated but don't have a linked Character.
- [x] New system trait: Token Sheets.
- This is not data per se, but rather a React component (possibly with subcomponents) that defines the layout and content of the Impersonated Token Context. Bundled systems will have one built by us, and anyone building a system module would be expected to provide one as well.
- For System 8 and 5e, this component will be constructed thus:
- In general, use the Mantine elements and styles that the Character Context uses; they should look similar
- Portrait or token art as background, with primary color extraction for background color and padding to show most of the image by default
- Bars above the name/class/level section, matching CurrentTokenBar styling
- Section 1: Token name, faction, class/level if present in token metadata
- Section 2: ability scores, rendered in the same way as the Character Context (e.g. "20 \n (+5)", clickable for rolls)
- Section 3: three values left-to-right: HP, AC, Speed (if present in token metadata)
- Section 4: a button for each action in the token, separated into action groups (when defined)
- Section 5: any token properties not already shown above, if present in token metadata. If none, skip this section.
- Section 6: Vision & Lighting, using the same UI approach as the Vision & Lighting tab in the Edit Token modal
- Section 7: Conditions, using the same UI approach as the Conditions tab in the Edit Token modal (except use two column layout for compactness)
- Section 8: Auras, using the same UI approach as the Auras tab in the Edit Token modal
- [x] When a token is impersonated, and has no linked character, show the system's Token Sheet in the left sidebar.
- [x] Add a fixed strip at the top of the Impersonated Token Context with the following buttons (same UI treatment as the Character Context):
- x button: "Stop Impersonating"
- ... button: "More Options" (same menu as Character Context)
- focus button: "Focus Camera on Token"
- zoom button: "Zoom Artwork"
- [x] In the name/class/level section of the Impersonated Token Context, add a link button to "Edit This Token" which opens the Edit Token modal for the impersonated token.
- [x] Change the Conditions section as follows:
- Only show conditions that are actually affecting the token.
- Show current conditions as a vertical list, with a trash icon to remove the condition (similar to the Auras section).
- Add an "Add Condition" button which opens a popover with a list of conditions to add.
- [x] Make the portrait image a regular image element instead of a background image, still matching the container width. Remove any padding-top that was used to ensure this image's visibility, as it's a block element now.
- [x] Make the gradient background fixed.
- [x] The x, ..., focus, and zoom buttons should be horizontally arranged in the new location
- [x] Bug: the special UI that shows when multiple tokens are selected doesn't appear when Impersonated Token Context is open (or it's behind it). When the left panel is already open, the Multi-Select UI should be positioned to the right of the left panel.
Persistent Power Actions
Re-architect the power → actions pipeline so derived IAction[] lives on the Character instead of being re-derived on every Add-to-Map / impersonate. Today convertCharacterToToken() (apps/web/src/utils/characterToToken.ts) does a parallel HTTP fan-out against the system-data proxy for every power's metadata.actions[] wiki path on every import — slow, fragile, and the result is thrown away each time. New architecture:
- Character power object owns its derived actions. Each power persists its full
IAction[]on the character row. - Derivation runs once, asynchronously, when powers change (add/remove/rename). Fire-and-forget on the server; lazy backfill on next save for legacy data.
- Action IDs are stable across re-derivation so saved tokens don't churn references — derived from
hash(powerName + actionPath). - Renderers (
PlayModeView) and converters (convertCharacterToToken) read straight fromcharacter.powers[i].actionsand only fall back to live derivation if absent.
Atomic TODOs (one commit each):
- [x] PPA-1: Extend
CharacterPowerwithactions?: IAction[]. Add field inpackages/shared/src/types/character.ts. No DB migration required —characters.powersis already atext("powers", { mode: "json" })column. Add a stable-ID helperderivePowerActionId(powerName, actionPath)exported from shared (hash-based, deterministic). Tests: type-shape assertion + ID-stability test. - [x] PPA-2: Extract power → IAction derivation into a shared, testable module. Today the logic is intertwined with
characterToToken.ts. MovefetchAndCreatePowerActions,fetchActionDocument, and the wiki-doc → IAction transform into a newapps/web/src/utils/derivePowerActions.ts(client-side stays — proxy is reachable only from the browser session for now). Makefetchinjectable so tests don't need network. Apply stable ID from PPA-1.characterToToken.tsbecomes a thin caller. Existing import tests must still pass. - [x] PPA-3: Server endpoint to derive + persist a character's power actions. New route
POST /api/characters/:id/derive-power-actionsinpackages/server-core/src/routes/characters*. On request, server reads the character, fetches each power's wiki docs (server-side fetch via system-data proxy logic), populatespower.actions, writes back. Idempotent. Honors stable IDs from PPA-1. Auth: same as character PATCH. Tests: derives correctly, preserves IDs, idempotent on re-run, no-ops for powers already derived (unless?force=1). Note on infra: project has no general background queue, so this endpoint is the unit of work. Caller (client) fires it post-save without awaiting. - [x] PPA-4: Client fires derivation when powers change. In whatever hook saves the character (
useCharacters/ inline PATCH sites), after a successful save where the powers diff differs (added a power, renamed, removed), firePOST /api/characters/:id/derive-power-actionsfire-and-forget. Surface failures only via a quietconsole.warn— the next save will retry. Tests pin: derivation called after a power is added, NOT called when only HP changed. - [x] PPA-5:
convertCharacterToTokenprefers stored actions. Modify the converter to usepower.actionsdirectly when present; only call the live-derivation fallback when missing (legacy characters not yet backfilled). On fallback path, schedule aderive-power-actionsPOST so subsequent imports are fast. Tests: stored actions used verbatim; missing actions → fallback + backfill triggered. - [x] PPA-6:
PlayModeViewreadscharacter.powers[i].actionsdirectly. No fetches. Unblocks Character Context TODO #7 (inline action buttons + (i) popovers can now render from in-memory data). Tests: pins direct access pattern. - [x] PPA-7: Manual refresh affordance for stale wiki content. Add a "Refresh power actions" menu item in the character header
…menu that POSTs to the derive endpoint with?force=1. Tests: button presence + correct URL.
Top-level Nav
Misc
- feat(genai v0.4.0 cross-cut): persist Generate-Monster-with-AI prompt onto the
token and pre-populate it in the genAI image source — The AI tab of the
Create Monster modal (
MonsterGenerator.handleCreateAiToken) now writes the user's natural-language prompt intotoken.metadata.aiPromptat token creation time.ArtworkSourceProps(in@purple-cat/shared) gains an optionalinitialPrompt?: stringfield that flows down fromTokenModalthroughArtworkTabandTokenArtworkEditorto the registered module's artwork-source component.SourceImageEditorDrawerforwards the same prop on its module-tab render path so callers likeCharacterDetailcan opt in later. Result: a user who picks "AI Generated" for a freshly-spawned monster sees the prompt textarea already filled with what they typed into the generator, so they can tweak it for the portrait/token art instead of retyping. Algorithmic monsters skip the field (no user-authored prompt exists). Regression:apps/web/src/components/UI/MonsterGenerator/MonsterAiPromptPersistence.test.ts. - feat(ai): add
GET /api/ai/image-models[/:provider]— New routes inpackages/server-core/src/routes/ai.tsthat own the curated image-gen model catalog (OpenAIgpt-image-1, Grokgrok-2-image, Gemini Imagen 4 family + Nano Banana family). The list is filtered by which providers have an API key configured — either viax-ai-key+x-ai-providerrequest headers (user-level) or theOPENAI_API_KEY/GROK_API_KEY/GEMINI_API_KEYserver env vars. This makes the platform the single source of truth for image-gen models;@purple-cat-games/genaiv0.4.0+ consumes this endpoint instead of maintaining its own hardcoded list, so model deprecations (like the recent DALL-E removal) only need a platform release. The existing/api/ai/models/:providerroute is unchanged — it filters for chat models, so it could not be reused. Curated rather than live-queried because providers don't expose a reliable image-only model list. Regression:packages/server-core/src/routes/ai.imageModels.test.ts.