diff --git a/docs/plans/2026-01-04-alt-overlay-scroll-fix.md b/docs/plans/2026-01-04-alt-overlay-scroll-fix.md new file mode 100644 index 0000000..4af800d --- /dev/null +++ b/docs/plans/2026-01-04-alt-overlay-scroll-fix.md @@ -0,0 +1,257 @@ +# Alt Text Overlay Scroll Fix + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Fix alt text overlay position drifting when page scrolls by moving overlay rendering from the badge to the carousel. + +**Architecture:** Move overlay from `grain-alt-badge` (which uses `position: fixed` with JS positioning) to `grain-image-carousel` (which renders it inside the slide with `position: absolute; inset: 0`). The badge becomes a simple button that emits an event. + +**Tech Stack:** Lit, Web Components, CSS + +--- + +### Task 1: Simplify grain-alt-badge to emit event + +**Files:** +- Modify: `src/components/atoms/grain-alt-badge.js` + +**Step 1: Remove overlay state and scroll listener logic** + +Replace the entire file with: + +```js +import { LitElement, html, css } from 'lit'; + +export class GrainAltBadge extends LitElement { + static properties = { + alt: { type: String } + }; + + static styles = css` + :host { + position: absolute; + bottom: 8px; + right: 8px; + z-index: 2; + } + .badge { + background: rgba(0, 0, 0, 0.7); + color: white; + font-size: 10px; + font-weight: 600; + padding: 2px 4px; + border-radius: 4px; + cursor: pointer; + user-select: none; + border: none; + font-family: inherit; + } + .badge:hover { + background: rgba(0, 0, 0, 0.85); + } + .badge:focus { + outline: 2px solid white; + outline-offset: 1px; + } + `; + + constructor() { + super(); + this.alt = ''; + } + + #handleClick(e) { + e.stopPropagation(); + this.dispatchEvent(new CustomEvent('alt-click', { + bubbles: true, + composed: true, + detail: { alt: this.alt } + })); + } + + render() { + if (!this.alt) return null; + + return html` + + `; + } +} + +customElements.define('grain-alt-badge', GrainAltBadge); +``` + +**Step 2: Verify badge still renders** + +Run the app, navigate to a gallery with alt text, confirm "ALT" button appears. + +--- + +### Task 2: Add overlay state and styles to grain-image-carousel + +**Files:** +- Modify: `src/components/organisms/grain-image-carousel.js` + +**Step 1: Add `_activeAltIndex` state property** + +In `static properties`, add: + +```js +static properties = { + photos: { type: Array }, + rkey: { type: String }, + _currentIndex: { state: true }, + _activeAltIndex: { state: true } +}; +``` + +**Step 2: Initialize state in constructor** + +Add to constructor: + +```js +this._activeAltIndex = null; +``` + +**Step 3: Add overlay styles** + +Add to `static styles` (after `.nav-arrow-right`): + +```css +.alt-overlay { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.75); + color: white; + padding: 16px; + font-size: 14px; + line-height: 1.5; + overflow-y: auto; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + box-sizing: border-box; + z-index: 3; + cursor: pointer; +} +``` + +--- + +### Task 3: Add overlay event handlers to carousel + +**Files:** +- Modify: `src/components/organisms/grain-image-carousel.js` + +**Step 1: Add handler for alt-click event** + +Add method: + +```js +#handleAltClick(e, index) { + e.stopPropagation(); + this._activeAltIndex = index; +} +``` + +**Step 2: Add handler for overlay click (dismiss)** + +Add method: + +```js +#handleOverlayClick(e) { + e.stopPropagation(); + this._activeAltIndex = null; +} +``` + +**Step 3: Dismiss overlay on slide change** + +Modify `#handleScroll` to clear overlay when swiping: + +```js +#handleScroll(e) { + const carousel = e.target; + const index = Math.round(carousel.scrollLeft / carousel.offsetWidth); + if (index !== this._currentIndex) { + this._currentIndex = index; + this._activeAltIndex = null; + } +} +``` + +--- + +### Task 4: Render overlay in slide template + +**Files:** +- Modify: `src/components/organisms/grain-image-carousel.js` + +**Step 1: Update slide template in render()** + +Replace the slide mapping (lines 153-162) with: + +```js +${this.photos.map((photo, index) => html` +
+ + ${photo.alt ? html` + this.#handleAltClick(e, index)} + > + ` : ''} + ${this._activeAltIndex === index ? html` +
+ ${photo.alt} +
+ ` : ''} +
+`)} +``` + +--- + +### Task 5: Manual testing + +**Step 1: Test overlay appears correctly** + +1. Navigate to a gallery with alt text +2. Click "ALT" button +3. Confirm overlay appears covering the image + +**Step 2: Test overlay dismisses on click** + +1. With overlay open, click the overlay +2. Confirm overlay closes + +**Step 3: Test overlay dismisses on swipe** + +1. Open alt overlay on first image +2. Swipe to second image +3. Confirm overlay closes + +**Step 4: Test scroll behavior (the bug fix)** + +1. Open alt overlay +2. Scroll the page up/down +3. Confirm overlay stays attached to the image (doesn't drift) + +--- + +### Task 6: Commit + +```bash +git add src/components/atoms/grain-alt-badge.js src/components/organisms/grain-image-carousel.js +git commit -m "fix: alt text overlay stays attached on page scroll + +Move overlay rendering from grain-alt-badge to grain-image-carousel. +The overlay now uses position:absolute within the slide instead of +position:fixed with JS positioning, so it naturally scrolls with content." +```