Files
initiative/specs/032-inline-confirm-buttons/research.md

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:

  • blur event 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:

  1. combatant-row.tsx: Replace the remove <Button> with <ConfirmButton>, move onRemove(id) to the onConfirm prop.
  2. turn-navigation.tsx: Replace the trash <Button> with <ConfirmButton>, pass onClearEncounter to onConfirm.
  3. use-encounter.ts: Remove window.confirm() from clearEncounter callback — 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.