5.0 KiB
Research: Inline Confirmation Buttons
R-001: Confirmation UX Pattern
Decision: Two-click inline confirmation with visual state transition (no modal, no popover).
Rationale: The button itself transforms in place — icon swaps to a checkmark, background turns red/danger, a scale pulse draws attention. This avoids the cognitive interruption of a modal dialog while still requiring deliberate confirmation. The pattern is well-established in tools like GitHub (delete branch buttons) and Notion (delete page).
Alternatives considered:
- Browser
window.confirm()— Already in use for clear encounter. Blocks the thread, looks outdated, inconsistent across browsers. Rejected. - Custom modal dialog — More disruptive than needed for single-button actions. Would require a new modal component. Rejected (over-engineered for icon buttons).
- Undo toast after immediate deletion — Simpler UX but requires implementing undo infrastructure in the domain layer. Out of scope per spec assumptions. Rejected.
- Hold-to-delete (long press) — Poor keyboard accessibility, no visual feedback during the hold, unfamiliar pattern for web apps. Rejected.
R-002: State Management Approach
Decision: Local useState boolean inside the ConfirmButton component, with useEffect for the auto-revert timer and click-outside/escape listeners.
Rationale: The confirm state is purely UI-local — it doesn't affect domain state, doesn't need to be persisted, and doesn't need to be shared between components. A simple boolean (isConfirming) is sufficient. The existing codebase already uses this exact pattern in HpAdjustPopover (click-outside detection, Escape handling, useCallback with cleanup).
Alternatives considered:
- Shared state / context — No need; each button is independent (FR-010). Rejected.
- Custom hook (
useConfirmButton) — Possible but premature. The logic is simple enough to live in the component. If more confirm buttons are added later, extraction to a hook is trivial. Rejected for now.
R-003: Animation Approach
Decision: CSS @keyframes animation registered as a Tailwind @utility, matching the existing animate-concentration-pulse and animate-slide-in-right patterns.
Rationale: The project already defines custom animations via @keyframes + @utility in index.css. A scale pulse (brief scale-up then back to normal) is lightweight and purely decorative — no JavaScript animation library needed.
Alternatives considered:
- JavaScript animation (Web Animations API) — Overkill for a simple pulse. Harder to coordinate with Tailwind classes. Rejected.
- Tailwind
transition-transform— Only handles transitions between states, not a pulse effect (scale up then back). Would need JS to toggle classes with timing. Rejected.
R-004: Destructive Color Tokens
Decision: Use existing --color-destructive (#ef4444) for the confirm-state background and keep --color-primary-foreground (#ffffff) for the icon in confirm state.
Rationale: The theme already defines --color-destructive and --color-hover-destructive. The confirm state needs a filled background (not just text color change) to be visually unmistakable. Using bg-destructive text-primary-foreground provides high contrast and matches the semantic meaning.
Alternatives considered:
bg-destructive/20(semi-transparent) — Too subtle for a confirmation state that must be immediately recognizable. Rejected.- New custom color token — Unnecessary; existing tokens suffice. Rejected.
R-005: Click-Outside Detection
Decision: mousedown event listener on document with ref.current.contains() check, cleaned up on unmount or state change.
Rationale: This is the exact pattern used by HpAdjustPopover in the existing codebase. It's proven, handles edge cases (clicking on other interactive elements), and cleans up properly.
Alternatives considered:
blurevent on button — Doesn't fire when clicking on non-focusable elements. Incomplete coverage. Rejected as sole mechanism (but focus loss is still handled via FR-005).- Third-party library (e.g.,
use-click-outside) — Unnecessary dependency for a simple pattern already implemented in the codebase. Rejected.
R-006: Integration Points
Decision: The ConfirmButton component wraps the existing Button component. Integration requires:
combatant-row.tsx: Replace the remove<Button>with<ConfirmButton>, moveonRemove(id)to theonConfirmprop.turn-navigation.tsx: Replace the trash<Button>with<ConfirmButton>, passonClearEncountertoonConfirm.use-encounter.ts: Removewindow.confirm()fromclearEncountercallback — confirmation is now handled by the UI component.
Rationale: Confirmation is a UI concern, not a business logic concern. Moving it from the hook (window.confirm) to the component (ConfirmButton) aligns with the layered architecture — the adapter layer handles user interaction, not the application layer.