Overhaul bottom bar: batch add, custom fields, stat block viewer

Unify the action bar into a single search input with inline bestiary
dropdown. Clicking a dropdown entry queues it with +/- count controls
and a confirm button; Enter or confirm adds N copies to combat.

When no bestiary match exists, optional Init/AC/MaxHP fields appear
for custom creatures. The eye icon opens a separate search dropdown
to preview stat blocks without leaving the add flow.

Fix batch-add bug where only the last creature got a creatureId by
using store.save() instead of setEncounter() in addFromBestiary.
Prevent dropdown buttons from stealing input focus so Enter confirms
the queued batch.

Remove the now-redundant BestiarySearch component.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-11 15:27:06 +01:00
parent 460c65bf49
commit b6e052f198
13 changed files with 931 additions and 213 deletions

View File

@@ -0,0 +1,48 @@
# Research: Bottom Bar Overhaul
## R-001: Batch Add via Existing Domain API
**Decision**: Call `addFromBestiary(result)` in a loop N times (once per queued count) rather than creating a new batch domain function.
**Rationale**: The existing `addFromBestiary` in `use-encounter.ts` already handles name auto-numbering (via `resolveCreatureName`), HP/AC assignment, and creatureId derivation. Calling it N times is correct because each call resolves the creature name against the updated combatant list, producing proper sequential numbering (e.g., "Goblin 1", "Goblin 2", "Goblin 3").
**Alternatives considered**:
- New domain-level `addBatchCombatants` function: Rejected because auto-numbering depends on seeing previously-added names, which the loop approach already handles. A batch function would duplicate logic.
- Application-layer batch use case: Rejected — no persistence coordination needed; the hook already manages state.
## R-002: Custom Creature Optional Fields (Initiative, AC, Max HP)
**Decision**: Extend the existing `onAddCombatant` callback (or create a sibling) to accept optional `{ initiative?: number; ac?: number; maxHp?: number }` alongside the name. The `use-encounter` hook's `addCombatant` flow will be extended to apply these fields to the newly created combatant.
**Rationale**: The domain `Combatant` type already has `initiative?`, `ac?`, and `maxHp?` fields. The domain `addCombatant` function creates a combatant with just `id` and `name`; the hook currently patches `currentHp`/`maxHp`/`ac` after creation for bestiary creatures. The same pattern works for custom creatures.
**Alternatives considered**:
- Extend domain `addCombatant` to accept optional fields: Rejected — the domain function is minimal by design (pure add with name validation). Post-creation patching is the established pattern.
- New domain function `addCustomCombatant`: Rejected — unnecessary complexity for fields that are just optional patches on the existing type.
## R-003: Stat Block Preview from Dropdown
**Decision**: Pass a `onViewStatBlock(result: SearchResult)` callback from App.tsx through ActionBar. When triggered, it derives the `CreatureId` from the search result (same `${source}:${slug}` pattern used in `addFromBestiary`) and opens the stat block panel for that creature.
**Rationale**: The stat block panel infrastructure already exists and accepts a `CreatureId`. The only new wiring is a callback from the dropdown row to the panel opener.
**Alternatives considered**:
- Inline stat block preview in the dropdown: Rejected — the dropdown is compact and the stat block panel already handles full rendering, source fetching, and caching.
## R-004: Unified Search Flow vs Separate BestiarySearch Component
**Decision**: Merge the separate `BestiarySearch` component's functionality into the action bar's existing inline search. Remove the toggle between "form mode" and "search mode" (`searchOpen` state). The action bar always shows a single input field that serves both purposes.
**Rationale**: The current action bar has two modes — a name input with inline suggestions, and a separate full `BestiarySearch` overlay toggled by the search button. The spec calls for a unified flow where the single input field shows bestiary results as you type (already working via `suggestions`), with click-to-queue behavior replacing the immediate add-on-click. The separate `BestiarySearch` component becomes redundant.
**Alternatives considered**:
- Keep both modes: Rejected — the spec explicitly merges the flows (dropdown opens on type, search button becomes stat block viewer).
## R-005: Queue State Management
**Decision**: Queue state is local React state in the action bar component: `{ result: SearchResult; count: number } | null`. No hook or context needed.
**Rationale**: The queue is purely ephemeral and scoped to the action bar's interaction lifecycle. It resets on confirm, escape, or when the queued creature leaves the search results.
**Alternatives considered**:
- Custom hook for queue state: Rejected — the state is simple (one nullable object) and doesn't need to be shared.