63 lines
5.0 KiB
Markdown
63 lines
5.0 KiB
Markdown
# 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.
|