Files
initiative/specs/024-fix-hp-popover-overflow/plan.md

5.4 KiB

Implementation Plan: Fix HP Popover Overflow

Branch: 024-fix-hp-popover-overflow | Date: 2026-03-09 | Spec: spec.md Input: Feature specification from /specs/024-fix-hp-popover-overflow/spec.md

Summary

The HP adjustment popover (HpAdjustPopover) overflows the right edge of the viewport when the trigger element (HP display) is positioned near the right side of the combatant row. This causes the popover to be partially hidden and introduces a horizontal scrollbar. The fix adds viewport-aware horizontal positioning using a useLayoutEffect measurement pattern, consistent with the existing ConditionPicker component which already handles vertical overflow.

Technical Context

Language/Version: TypeScript 5.8 (strict mode, verbatimModuleSyntax) Primary Dependencies: React 19, Tailwind CSS v4, Lucide React (icons) Storage: N/A (no storage changes — purely presentational fix) Testing: Vitest Target Platform: Web browser (desktop + mobile) Project Type: Web application (monorepo: apps/web + packages/domain + packages/application) Performance Goals: N/A (simple layout fix) Constraints: No heavy positioning libraries; lightweight DOM measurement only Scale/Scope: Single component fix (~20 lines changed)

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 — adapter-layer only
II. Layered Architecture PASS Change is entirely in the adapter layer (web UI component)
III. Agent Boundary N/A No agent involvement
IV. Clarification-First PASS Bug fix with clear spec, no ambiguity
V. Escalation Gates PASS Scope matches spec exactly
VI. MVP Baseline Language PASS No scope restrictions involved
VII. No Gameplay Rules PASS No gameplay mechanics involved

All gates pass. No violations.

Project Structure

Documentation (this feature)

specs/024-fix-hp-popover-overflow/
├── plan.md              # This file
├── research.md          # Phase 0 output
└── tasks.md             # Phase 2 output (/speckit.tasks command)

Source Code (repository root)

apps/web/src/components/
├── hp-adjust-popover.tsx    # FIX: Add viewport-aware horizontal positioning
└── combatant-row.tsx        # Context: ClickableHp renders popover (relative parent)

Structure Decision: This is a single-component bug fix within the existing apps/web adapter layer. No new files, packages, or architectural changes needed.

Design

Root Cause

The HpAdjustPopover uses absolute positioning relative to its parent ClickableHp container (which has relative positioning). The popover renders to the right of the HP value display. When the HP display is near the right edge of the viewport (common since HP is in one of the rightmost grid columns), the popover extends beyond the viewport boundary.

The combatant list container has overflow-y-auto but no overflow-x: hidden, so the popover overflow manifests as a horizontal scrollbar on the page.

Approach: position: fixed with useLayoutEffect viewport clamping

Switch the popover from position: absolute to position: fixed and use a useLayoutEffect hook to:

  1. Measure the trigger element's viewport rect via parentElement.getBoundingClientRect()
  2. Measure the popover's own width via getBoundingClientRect()
  3. Position the popover just below the trigger (trigger.bottom + 4)
  4. Clamp the left coordinate so the popover stays within viewport bounds (8px padding)
  5. Render with visibility: hidden until positioned to avoid a flash

This approach was chosen because the popover's scroll container ancestor (overflow-y-auto) clips absolutely-positioned children, making position: absolute with right-0 or translateX ineffective — the popover gets clipped rather than repositioned.

Implementation Detail

  • Replace absolute with fixed positioning on the popover container
  • Add useLayoutEffect that reads parentElement.getBoundingClientRect() for the trigger position and el.getBoundingClientRect() for the popover width
  • Compute left = trigger.left, clamped to [8, vw - popover.width - 8] using document.documentElement.clientWidth
  • Set top = trigger.bottom + 4
  • Store computed { top, left } in state; render visibility: hidden until positioned
  • Click-outside detection continues to work since document.addEventListener("mousedown") is not affected by positioning scheme

Alternatives Considered

  • CSS-only overflow-x: hidden on ancestor: Would clip the popover rather than reposition it, and could hide other legitimate content.
  • Heavy positioning libraries (Floating UI, Popper): Overkill for a small popover with a simple overflow case.
  • position: absolute with right-0 flip (ConditionPicker pattern): The popover's relative parent is too small (just the HP number), so right-0 barely shifts the popover. Also, the scroll container's overflow clipping still clips the popover.
  • position: absolute with translateX: Same clipping issue — the scroll container clips the popover regardless of transform.

Post-Design Constitution Re-check

All gates still pass. The fix is entirely in the adapter layer, uses no domain logic, and stays within spec scope.