Implement the 011-quick-hp-input feature that adds an inline damage/heal numeric input per combatant row with mode toggle, keyboard workflow, and visual distinction

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lukas
2026-03-05 22:43:26 +01:00
parent 1c40bf7889
commit a0d85a07e3
10 changed files with 644 additions and 0 deletions

View File

@@ -61,6 +61,8 @@ The constitution (`.specify/memory/constitution.md`) governs all feature work:
- TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite 6, Biome 2.0, Vites (009-combatant-hp)
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Vite 6, Tailwind CSS v4, shadcn/ui, Lucide React (icons) (010-ui-baseline)
- N/A (no storage changes — localStorage persistence unchanged) (010-ui-baseline)
- TypeScript 5.8 (strict mode, verbatimModuleSyntax) + React 19, Vite 6, Tailwind CSS v4, shadcn/ui-style components, Lucide React (icons) (011-quick-hp-input)
- N/A (no storage changes -- existing localStorage persistence handles HP via `adjustHpUseCase`) (011-quick-hp-input)
## Recent Changes
- 003-remove-combatant: Added TypeScript 5.x (strict mode, verbatimModuleSyntax) + React 19, Vite

View File

@@ -2,6 +2,7 @@ import type { CombatantId } from "@initiative/domain";
import { X } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { cn } from "../lib/utils";
import { QuickHpInput } from "./quick-hp-input";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
@@ -228,6 +229,9 @@ export function CombatantRow({
<span className="text-sm tabular-nums text-muted-foreground">/</span>
)}
<MaxHpInput maxHp={maxHp} onCommit={(v) => onSetHp(id, v)} />
{maxHp !== undefined && (
<QuickHpInput combatantId={id} onAdjustHp={onAdjustHp} />
)}
</div>
{/* Actions */}

View File

@@ -0,0 +1,100 @@
import type { CombatantId } from "@initiative/domain";
import { Heart, Sword } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { cn } from "../lib/utils";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
type Mode = "damage" | "heal";
interface QuickHpInputProps {
readonly combatantId: CombatantId;
readonly disabled?: boolean;
readonly onAdjustHp: (id: CombatantId, delta: number) => void;
}
export function QuickHpInput({
combatantId,
disabled,
onAdjustHp,
}: QuickHpInputProps) {
const [mode, setMode] = useState<Mode>("damage");
const [inputValue, setInputValue] = useState("");
const inputRef = useRef<HTMLInputElement>(null);
const apply = useCallback(() => {
if (inputValue === "") return;
const n = Number.parseInt(inputValue, 10);
if (Number.isNaN(n) || n <= 0) return;
const delta = mode === "damage" ? -n : n;
onAdjustHp(combatantId, delta);
setInputValue("");
}, [inputValue, mode, combatantId, onAdjustHp]);
const toggleMode = useCallback(() => {
setMode((m) => (m === "damage" ? "heal" : "damage"));
}, []);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
apply();
} else if (e.key === "Escape") {
setInputValue("");
} else if (e.key === "Tab") {
e.preventDefault();
toggleMode();
}
},
[apply, toggleMode],
);
const isDamage = mode === "damage";
return (
<div className="flex items-center gap-0.5">
<Button
type="button"
variant="ghost"
size="icon"
disabled={disabled}
className={cn(
"h-7 w-7 shrink-0",
isDamage
? "text-red-400 hover:bg-red-950 hover:text-red-300"
: "text-emerald-400 hover:bg-emerald-950 hover:text-emerald-300",
)}
onClick={toggleMode}
title={
isDamage
? "Damage mode (click to switch to heal)"
: "Heal mode (click to switch to damage)"
}
aria-label={isDamage ? "Switch to heal mode" : "Switch to damage mode"}
>
{isDamage ? <Sword size={14} /> : <Heart size={14} />}
</Button>
<Input
ref={inputRef}
type="text"
inputMode="numeric"
disabled={disabled}
value={inputValue}
placeholder={isDamage ? "Dmg" : "Heal"}
className={cn(
"h-7 w-[7ch] text-center text-sm tabular-nums",
isDamage
? "focus-visible:ring-red-500/50"
: "focus-visible:ring-emerald-500/50",
)}
onChange={(e) => {
const v = e.target.value;
if (v === "" || /^\d+$/.test(v)) {
setInputValue(v);
}
}}
onKeyDown={handleKeyDown}
/>
</div>
);
}

View File

@@ -0,0 +1,36 @@
# Specification Quality Checklist: Quick HP Input
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-03-05
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
- The Tab key for mode toggle is noted as an assumption -- exact keybinding is deferred to implementation.
- FR-013 (replacing +/- buttons) is a design decision documented in Assumptions.

View File

@@ -0,0 +1,87 @@
# Data Model: Quick HP Input
## Existing Entities (unchanged)
### Combatant
| Field | Type | Notes |
|-------|------|-------|
| id | CombatantId (branded string) | Unique identifier |
| name | string | Display name |
| initiative | number or undefined | Turn order value |
| maxHp | number or undefined | Positive integer >= 1, optional |
| currentHp | number or undefined | Integer in [0, maxHp], optional |
No changes to the domain `Combatant` type. HP fields already support the full range of operations needed.
### Encounter
| Field | Type | Notes |
|-------|------|-------|
| combatants | readonly Combatant[] | Ordered list |
| currentTurnIndex | number | Active combatant index |
| roundNumber | number | Current round |
No changes to the domain `Encounter` type.
## Existing Domain Events (unchanged)
### CurrentHpAdjusted
| Field | Type | Notes |
|-------|------|-------|
| type | "CurrentHpAdjusted" | Discriminant |
| combatantId | CombatantId | Target combatant |
| previousHp | number | HP before adjustment |
| newHp | number | HP after adjustment (clamped) |
| delta | number | Signed delta applied |
This event is already emitted by `adjustHp()` and fully supports the quick input feature. A negative delta represents damage, a positive delta represents healing.
## New UI-Only State (component-local, not persisted)
### QuickHpInput component state
| State | Type | Default | Notes |
|-------|------|---------|-------|
| mode | "damage" or "heal" | "damage" | Toggle between subtract and add |
| inputValue | string | "" | Raw text in the input field |
This state is local to the React component and does not persist across page reloads. The mode resets to "damage" on component mount. The input value clears after each successful application.
## State Transitions
### Apply damage
```
User enters number N in damage mode -> Enter
-> adjustHp(encounter, combatantId, -N)
-> currentHp = max(0, currentHp - N)
-> input clears
```
### Apply healing
```
User enters number N in heal mode -> Enter
-> adjustHp(encounter, combatantId, +N)
-> currentHp = min(maxHp, currentHp + N)
-> input clears
```
### Toggle mode
```
User clicks toggle or presses Tab while focused
-> mode flips: "damage" <-> "heal"
-> input value preserved (not cleared)
-> visual treatment updates immediately
```
### Dismiss
```
User presses Escape
-> input clears
-> no HP change applied
```

View File

@@ -0,0 +1,80 @@
# Implementation Plan: Quick HP Input
**Branch**: `011-quick-hp-input` | **Date**: 2026-03-05 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `/specs/011-quick-hp-input/spec.md`
## Summary
Add an always-visible inline quick HP input per combatant that allows the game master to type a damage or healing number and immediately apply it as a delta to current HP. The domain layer already provides `adjustHp(encounter, combatantId, delta)` with full clamping logic, so no domain or application changes are needed. This is a UI-only feature: a new `QuickHpInput` component in the web adapter layer with damage/heal mode toggle, keyboard-driven workflow, and clear visual distinction between modes.
## Technical Context
**Language/Version**: TypeScript 5.8 (strict mode, verbatimModuleSyntax)
**Primary Dependencies**: React 19, Vite 6, Tailwind CSS v4, shadcn/ui-style components, Lucide React (icons)
**Storage**: N/A (no storage changes -- existing localStorage persistence handles HP via `adjustHpUseCase`)
**Testing**: Vitest (domain pure-function tests); UI component tested via existing patterns
**Target Platform**: Browser (single-user, local-first)
**Project Type**: Web application (monorepo: packages/domain, packages/application, apps/web)
**Performance Goals**: N/A (single-user, instant local state updates)
**Constraints**: Must follow layered architecture (domain -> application -> adapters); UI-only changes in apps/web
**Scale/Scope**: Single combatant row component enhancement
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
| Principle | Status | Notes |
|-----------|--------|-------|
| I. Deterministic Domain Core | PASS | No domain changes. Existing `adjustHp` is a pure function. |
| II. Layered Architecture | PASS | All changes are in the adapter layer (apps/web). Domain and application layers unchanged. |
| III. Agent Boundary | N/A | No agent features involved. |
| IV. Clarification-First | PASS | Clarifications completed in spec (visibility model, direct entry coexistence). |
| V. Escalation Gates | PASS | All requirements are in the spec. |
| VI. MVP Baseline Language | PASS | Exclusions use "MVP baseline does not include" phrasing. |
| VII. No Gameplay Rules | PASS | No gameplay mechanics in constitution. |
## Project Structure
### Documentation (this feature)
```text
specs/011-quick-hp-input/
├── spec.md
├── plan.md # This file
├── research.md # Phase 0 output
├── data-model.md # Phase 1 output
├── quickstart.md # Phase 1 output
└── tasks.md # Phase 2 output (via /speckit.tasks)
```
### Source Code (repository root)
```text
apps/web/src/
├── components/
│ ├── combatant-row.tsx # Modified: integrate QuickHpInput
│ ├── quick-hp-input.tsx # NEW: inline damage/heal input component
│ └── ui/
│ ├── button.tsx # Existing (may use for toggle)
│ └── input.tsx # Existing (base input styling)
├── hooks/
│ └── use-encounter.ts # Unchanged (adjustHp already exposed)
└── App.tsx # Unchanged (props already passed through)
packages/domain/src/
├── adjust-hp.ts # Unchanged (already supports +/- deltas)
├── set-hp.ts # Unchanged
├── types.ts # Unchanged
└── __tests__/
└── adjust-hp.test.ts # Unchanged (domain logic covered)
packages/application/src/
├── adjust-hp-use-case.ts # Unchanged
└── ports.ts # Unchanged
```
**Structure Decision**: UI-only change. Single new component (`quick-hp-input.tsx`) added to the existing component directory. `combatant-row.tsx` modified to integrate it alongside the existing direct HP entry. No new directories needed.
## Complexity Tracking
No violations to justify. All changes are within the existing adapter layer, using established patterns.

View File

@@ -0,0 +1,49 @@
# Quickstart: Quick HP Input
## What this feature does
Adds an inline damage/heal numeric input to each combatant row. Instead of editing the absolute HP value, the game master types a number (e.g., "7") and the system applies it as damage (subtract) or healing (add) to the combatant's current HP.
## Key files to modify
1. **`apps/web/src/components/quick-hp-input.tsx`** (NEW) -- The main component: numeric input + damage/heal mode toggle.
2. **`apps/web/src/components/combatant-row.tsx`** (MODIFY) -- Integrate `QuickHpInput` into the combatant row layout alongside existing HP display.
## Key files to understand (read-only)
- `packages/domain/src/adjust-hp.ts` -- Domain function that applies signed deltas (negative = damage, positive = heal)
- `packages/domain/src/types.ts` -- `Combatant` type with optional `maxHp`/`currentHp`
- `apps/web/src/hooks/use-encounter.ts` -- `adjustHp(id, delta)` hook already exposed
- `apps/web/src/components/ui/input.tsx` -- Base input component for consistent styling
- `apps/web/src/components/ui/button.tsx` -- Base button component for toggle
## How the pieces connect
```
QuickHpInput (new component)
|-- User types "7" in damage mode, presses Enter
|-- Calls onAdjustHp(combatantId, -7)
|
CombatantRow
|-- Passes onAdjustHp prop to QuickHpInput
|
useEncounter hook
|-- adjustHp(id, delta) -> adjustHpUseCase -> domain adjustHp()
|-- Domain clamps result to [0, maxHp]
|-- State updates, localStorage persists
```
## Running the feature locally
```bash
pnpm --filter web dev # Start Vite dev server at localhost:5173
```
## Running tests
```bash
pnpm check # Full quality gate (format + lint + typecheck + test)
pnpm test # All tests
```
No new domain tests needed -- `adjustHp` is already fully tested. New tests (if any) would be component-level tests for `QuickHpInput` keyboard behavior and mode toggling.

View File

@@ -0,0 +1,44 @@
# Research: Quick HP Input
## R1: Domain layer readiness
**Decision**: No domain changes needed.
**Rationale**: The existing `adjustHp(encounter, combatantId, delta)` function already accepts positive deltas (healing) and negative deltas (damage) with full clamping to `[0, maxHp]`. The function validates non-zero integer deltas and returns errors for invalid input. The `adjustHpUseCase` in the application layer already wraps this with store persistence.
**Alternatives considered**: Adding a dedicated `applyDamage`/`applyHeal` domain function pair was considered but rejected -- it would duplicate `adjustHp` logic. The direction (damage vs heal) is a UI concern; the domain only needs a signed delta.
## R2: UI component pattern
**Decision**: Create a standalone `QuickHpInput` React component that renders inline within `CombatantRow`.
**Rationale**: The component needs its own local state (input value, damage/heal mode) and keyboard event handling. Keeping it as a separate component follows the existing pattern (e.g., `CurrentHpInput`, `MaxHpInput` are already sub-components within `combatant-row.tsx`). However, since `QuickHpInput` has more complex behavior (mode toggle, visual states), it warrants its own file.
**Alternatives considered**: Inline implementation within `combatant-row.tsx` was considered but rejected due to the component's interaction complexity (mode state, keyboard handling, visual treatments).
## R3: Damage/heal mode toggle UX
**Decision**: Use a segmented toggle button adjacent to the input field. Damage mode uses a red/destructive color treatment; heal mode uses a green/positive color treatment. A Lucide icon (e.g., `Sword` or `Minus` for damage, `Heart` or `Plus` for heal) reinforces the mode.
**Rationale**: Color + icon provides two visual channels for quick recognition during combat. A segmented toggle is a single-click interaction (FR-004) and is familiar UI pattern. Tab key toggles mode when input is focused (keyboard workflow).
**Alternatives considered**:
- Dropdown select: Too slow (2 clicks minimum).
- Two separate inputs (one for damage, one for heal): More screen space, but eliminates mode confusion entirely. Rejected for space efficiency in the combatant row.
- Prefix sign (type "-7" for damage, "+5" for heal): Requires extra keystroke per entry and is error-prone.
## R4: Keyboard workflow
**Decision**: Enter confirms and applies the value, Escape clears without applying, Tab toggles damage/heal mode while keeping focus in the input.
**Rationale**: Enter-to-confirm is the standard web input pattern. Tab for mode toggle prevents the user from leaving the input (Tab's default behavior would move focus away). Escape-to-cancel follows dialog dismissal convention.
**Alternatives considered**: Using a keyboard shortcut like `D`/`H` keys was considered but rejected since the input field accepts numeric characters and letter keys would conflict.
## R5: Coexistence with direct HP entry
**Decision**: The existing `CurrentHpInput` (absolute value entry) and `MaxHpInput` remain unchanged. The new `QuickHpInput` is added as an additional control in the combatant row, visually grouped with the HP display.
**Rationale**: Per clarification, both direct entry and quick input must coexist. Direct entry is for GM overrides/corrections; quick input is for combat flow. They call different interactions: direct entry computes a delta from old-to-new absolute values, while quick input directly provides the delta.
**Alternatives considered**: Merging both into a single smart input was considered but rejected -- it would complicate the interaction model and violate the principle of minimal complexity.

View File

@@ -0,0 +1,133 @@
# Feature Specification: Quick HP Input
**Feature Branch**: `011-quick-hp-input`
**Created**: 2026-03-05
**Status**: Draft
**Input**: User description: "Add a feature to quickly input healing and damage numbers and subtract from the current HP on input with nice ux and sleek design. Make it easy to use in the heat of combat."
## User Scenarios & Testing
### User Story 1 - Apply Damage via Quick Input (Priority: P1)
As a game master in the heat of combat, I want to type a damage number and immediately apply it to a combatant's HP so that I can keep up with fast-paced encounters without mental arithmetic.
**Why this priority**: Applying damage is the most frequent HP interaction during combat. A dedicated numeric input for damage amounts is the core value proposition.
**Independent Test**: Can be fully tested by having a combatant with HP set, entering a damage number, and verifying current HP decreases by that amount.
**Acceptance Scenarios**:
1. **Given** a combatant has 20/20 HP, **When** the user focuses the damage input and enters 7, **Then** current HP decreases to 13.
2. **Given** a combatant has 10/20 HP, **When** the user enters damage of 15, **Then** current HP is clamped to 0 (not negative).
3. **Given** a combatant has 5/20 HP, **When** the user enters damage of 0, **Then** the input is rejected and HP remains unchanged.
4. **Given** a combatant has 20/20 HP, **When** the user enters damage and confirms, **Then** the input field clears and is ready for the next entry.
---
### User Story 2 - Apply Healing via Quick Input (Priority: P1)
As a game master, I want to type a healing number and immediately apply it to a combatant's HP so that I can process healing spells and potions quickly.
**Why this priority**: Healing is the second most common HP interaction during combat and completes the damage/heal pair.
**Independent Test**: Can be fully tested by having a combatant with reduced HP, entering a healing number, and verifying current HP increases by that amount.
**Acceptance Scenarios**:
1. **Given** a combatant has 10/20 HP, **When** the user focuses the heal input and enters 5, **Then** current HP increases to 15.
2. **Given** a combatant has 18/20 HP, **When** the user enters healing of 10, **Then** current HP is clamped to 20 (max HP).
3. **Given** a combatant has 20/20 HP, **When** the user enters healing of 5, **Then** current HP remains at 20.
4. **Given** a combatant has 0/20 HP, **When** the user enters healing of 8, **Then** current HP increases to 8.
---
### User Story 3 - Toggle Between Damage and Heal Modes (Priority: P1)
As a game master, I want a clear and fast way to switch between applying damage and healing so that I don't accidentally heal when I mean to damage (or vice versa).
**Why this priority**: Mode clarity is essential to prevent errors during fast-paced combat. A misapplied heal or damage can disrupt the game.
**Independent Test**: Can be fully tested by toggling between modes and verifying the correct operation is applied.
**Acceptance Scenarios**:
1. **Given** the quick input is in damage mode, **When** the user toggles to heal mode, **Then** the input visually indicates heal mode and subsequent entries add to HP.
2. **Given** the quick input is in heal mode, **When** the user toggles to damage mode, **Then** the input visually indicates damage mode and subsequent entries subtract from HP.
3. **Given** the quick input is active, **When** the user looks at the input, **Then** the current mode (damage or heal) is clearly distinguishable at a glance.
---
### User Story 4 - Keyboard-Driven Workflow (Priority: P2)
As a game master, I want to enter damage/healing values using only the keyboard so that I can work as fast as possible during tense combat moments.
**Why this priority**: Speed is critical during combat encounters. Mouse-only interactions slow down the game master.
**Independent Test**: Can be fully tested by using only keyboard interactions to apply damage and healing values.
**Acceptance Scenarios**:
1. **Given** the quick input is focused, **When** the user types a number and presses Enter, **Then** the value is applied immediately and the input clears.
2. **Given** the quick input is focused, **When** the user presses Escape, **Then** the input is dismissed without applying any change.
3. **Given** the quick input is focused in damage mode, **When** the user presses Tab, **Then** the mode toggles to heal (or vice versa).
---
### Edge Cases
- What happens when the user enters a non-numeric value? The input rejects non-numeric characters; only digits are accepted.
- What happens when the user enters a very large number (e.g., 99999)? The value is applied normally; clamping to 0 or max HP ensures no invalid state.
- What happens when the combatant has no HP tracking (no max HP set)? The quick input controls are not available for that combatant.
- What happens when the user submits an empty input? No change is applied; the input remains ready for the next entry.
- What happens when the user rapidly enters multiple values? Each entry is applied sequentially; no values are lost or merged.
## Clarifications
### Session 2026-03-05
- Q: How is the quick input presented per combatant? → A: Always visible inline in each combatant row in a compact resting state; becomes fully interactive on focus. No extra click needed to reveal it.
- Q: Do +/- 1 buttons currently exist? → A: No. The +/- 1 buttons were removed in a prior iteration and do not exist in the current product.
- Q: Should direct editing of the current HP absolute value still be possible? → A: Yes. Keep direct HP entry alongside the new quick damage/heal input. Both methods are available.
## Requirements
### Functional Requirements
- **FR-001**: The system MUST provide an always-visible inline numeric input per combatant for entering damage and healing amounts. The input MUST be in a compact resting state and become fully interactive on focus.
- **FR-002**: The system MUST support two modes: damage (subtracts from current HP) and heal (adds to current HP).
- **FR-003**: Damage mode MUST be the default mode when the input is first activated.
- **FR-004**: The user MUST be able to toggle between damage and heal modes with a single interaction.
- **FR-005**: When a damage value is confirmed, the system MUST subtract the entered amount from the combatant's current HP, clamping to 0.
- **FR-006**: When a healing value is confirmed, the system MUST add the entered amount to the combatant's current HP, clamping to max HP.
- **FR-007**: After a value is confirmed, the input MUST clear automatically to accept the next entry.
- **FR-008**: The input MUST only accept positive integers. Zero, negative numbers, and non-numeric input MUST be rejected.
- **FR-009**: The input MUST be confirmable via Enter key press.
- **FR-010**: The input MUST be dismissable via Escape key without applying changes.
- **FR-011**: The current mode MUST be visually distinct -- damage and heal modes MUST have clearly different visual treatments (color, icon, or label).
- **FR-012**: The quick input MUST only be available for combatants that have HP tracking active (max HP is set).
- **FR-013**: The quick input is the primary method for adjusting current HP during combat. Direct editing of the current HP absolute value MUST remain available alongside the quick input.
- **FR-014**: The design MUST be optimized for speed of use during combat -- minimal clicks, clear affordances, and immediate feedback.
- **FR-015**: The direct HP entry and the quick damage/heal input MUST coexist without conflict. Direct entry sets an absolute value; quick input applies a delta.
### Key Entities
- **HP Adjustment**: A value applied to a combatant's current HP. Has an amount (positive integer) and a direction (damage or heal).
## Success Criteria
### Measurable Outcomes
- **SC-001**: A user can apply a damage or healing value to a combatant in under 3 seconds (type number, confirm).
- **SC-002**: Current HP never exceeds max HP or drops below 0 after any quick input operation.
- **SC-003**: The active mode (damage or heal) is identifiable within 1 second of looking at the interface.
- **SC-004**: Users can complete a full damage/heal cycle (apply damage, switch mode, apply healing) using only the keyboard.
- **SC-005**: The quick input workflow requires no more than 2 interactions to apply a value (enter number + confirm).
## Assumptions
- The quick input is the primary HP adjustment method during combat. There are no existing +/- buttons. Direct HP entry (absolute value) remains available for overrides and corrections.
- There is no undo/redo for HP changes in the MVP baseline.
- There is no damage type tracking (fire, slashing, etc.) in the MVP baseline.
- There is no damage resistance/vulnerability calculation in the MVP baseline.
- There is no hit log or damage history in the MVP baseline.
- The Tab key toggling between damage/heal mode is an assumption for keyboard workflow. The exact keybinding is an implementation detail.

View File

@@ -0,0 +1,109 @@
# Tasks: Quick HP Input
**Input**: Design documents from `/specs/011-quick-hp-input/`
**Prerequisites**: plan.md (required), spec.md (required), research.md, data-model.md
**Tests**: No test tasks included (not explicitly requested in spec). Domain logic is already fully tested via `adjustHp.test.ts`.
**Organization**: US1 (damage), US2 (healing), and US3 (mode toggle) are all P1 and form a single inseparable component (`QuickHpInput`). They are combined into one phase since you cannot meaningfully implement damage input without the mode concept. US4 (keyboard workflow) is P2 and layered on top.
## Format: `[ID] [P?] [Story] Description`
- **[P]**: Can run in parallel (different files, no dependencies)
- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
---
## Phase 1: Core Quick HP Input Component (US1 + US2 + US3, Priority: P1)
**Goal**: Deliver a working inline damage/heal input per combatant with mode toggle and visual distinction. Covers the primary combat workflow: type a number, apply as damage or healing.
**Independent Test**: Set a combatant's max HP to 20, type "7" in damage mode and confirm -- HP drops to 13. Toggle to heal mode, type "5" and confirm -- HP rises to 18. Mode is visually distinguishable at a glance.
### Implementation
- [x] T001 [US1] [US2] [US3] Create `QuickHpInput` component skeleton with local state (`mode: "damage" | "heal"`, `inputValue: string`), props interface (`combatantId`, `disabled`, `onAdjustHp`), and basic render structure in `apps/web/src/components/quick-hp-input.tsx`
- [x] T002 [US1] [US2] Implement numeric input field that only accepts digit characters, parses to integer on confirm, computes signed delta based on mode (damage = negative, heal = positive), calls `onAdjustHp(combatantId, delta)`, and clears input after successful application. Reject zero and empty input. File: `apps/web/src/components/quick-hp-input.tsx`
- [x] T003 [US3] Implement damage/heal mode toggle button adjacent to the input field. Use distinct visual treatments: damage mode with destructive color (red tones) and a damage icon (e.g., `Sword` or `Minus` from Lucide), heal mode with positive color (green tones) and a heal icon (e.g., `Heart` or `Plus` from Lucide). Toggle with a single click. File: `apps/web/src/components/quick-hp-input.tsx`
- [x] T004 [US1] [US2] [US3] Integrate `QuickHpInput` into `CombatantRow` grid layout. Add it alongside the existing `CurrentHpInput`/`MaxHpInput` display. Pass `onAdjustHp` prop through. Only render when combatant has HP tracking active (`maxHp` is defined). Ensure the compact resting state fits the existing row height. File: `apps/web/src/components/combatant-row.tsx`
**Checkpoint**: Damage and healing can be applied via the inline input with mouse interaction. Mode toggle works with clear visual distinction. Direct HP entry still functions alongside.
---
## Phase 2: Keyboard-Driven Workflow (US4, Priority: P2)
**Goal**: Enable full keyboard-driven damage/heal workflow so the game master never needs to reach for the mouse during combat.
**Independent Test**: Focus the quick input, type "7", press Enter -- value applies and input clears. Press Escape -- input clears without applying. Press Tab -- mode toggles between damage and heal while keeping focus.
### Implementation
- [x] T005 [US4] Add `onKeyDown` handler to the quick input: Enter confirms and applies the value (same as existing confirm logic), Escape clears the input without applying. File: `apps/web/src/components/quick-hp-input.tsx`
- [x] T006 [US4] Add Tab key handling to toggle mode while preventing default tab-to-next-element behavior. When Tab is pressed while the input is focused, flip mode (`damage` <-> `heal`) and keep focus in the input. File: `apps/web/src/components/quick-hp-input.tsx`
**Checkpoint**: Full keyboard workflow functional -- Enter to apply, Escape to dismiss, Tab to toggle mode. No mouse required.
---
## Phase 3: Polish & Cross-Cutting Concerns
**Purpose**: Final validation, visual polish, and quality gate
- [x] T007 Verify compact resting state styling: input should be minimal/subtle when not focused and expand to full interactive state on focus. Ensure consistent look with existing combatant row elements (same height, alignment, font). File: `apps/web/src/components/quick-hp-input.tsx`
- [x] T008 Run `pnpm check` (knip + format + lint + typecheck + test) and fix any issues. Ensure no unused exports, no type errors, and all existing tests still pass.
---
## Dependencies & Execution Order
### Phase Dependencies
- **Phase 1 (Core Component)**: Can start immediately -- no setup or foundational work needed. Domain `adjustHp` and `useEncounter` hook already exist.
- **Phase 2 (Keyboard)**: Depends on Phase 1 completion (T001-T004). Keyboard handlers are added to the existing component.
- **Phase 3 (Polish)**: Depends on Phase 2 completion.
### User Story Dependencies
- **US1 (Damage) + US2 (Healing) + US3 (Mode Toggle)**: Implemented together in Phase 1 as they form one inseparable component. Cannot apply damage without the mode concept, cannot toggle without both modes.
- **US4 (Keyboard)**: Depends on US1+US2+US3 (the component must exist before adding keyboard handlers).
### Within Phase 1
```
T001 (skeleton) → T002 (input logic) → T004 (integration)
T001 (skeleton) → T003 (toggle UI) → T004 (integration)
```
T002 and T003 can run in parallel after T001 (different concerns in the same file, but T002 is input logic and T003 is toggle UI -- recommend sequential to avoid merge conflicts in the same file).
### Parallel Opportunities
Limited parallelism due to single-file component. Recommended sequential execution:
`T001 → T002 → T003 → T004 → T005 → T006 → T007 → T008`
---
## Implementation Strategy
### MVP First (Phase 1 Only)
1. Complete T001-T004 (core component + integration)
2. **STOP and VALIDATE**: Test damage and healing with mouse clicks
3. This delivers the core value: quick numeric HP adjustment
### Full Feature
1. Complete Phase 1 → Validate
2. Complete Phase 2 (keyboard workflow) → Validate
3. Complete Phase 3 (polish + quality gate) → Ready for commit
---
## Notes
- No domain or application layer changes needed. All tasks are in `apps/web/src/`.
- The existing `adjustHp(id, delta)` from `useEncounter` hook handles clamping, persistence, and event emission.
- The `QuickHpInput` component only needs to: parse user input to integer, determine sign from mode, and call `onAdjustHp`.
- Existing `CurrentHpInput` and `MaxHpInput` remain untouched per clarification.