WIP PWA for Grain
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

docs: add scroll-to-top button implementation plan

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+265
+265
docs/plans/2025-12-29-scroll-to-top.md
··· 1 + # Scroll-to-Top Button Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Add a floating action button that appears when scrolled down, scrolls to top and refreshes feed on click. 6 + 7 + **Architecture:** New `grain-scroll-to-top` atom component with visibility controlled by parent. Timeline tracks scroll position and handles click to scroll + refresh. 8 + 9 + **Tech Stack:** Lit, CSS custom properties, existing `grain-icon` component 10 + 11 + --- 12 + 13 + ### Task 1: Add arrow-up icon to grain-icon 14 + 15 + **Files:** 16 + - Modify: `src/components/atoms/grain-icon.js:4-26` 17 + 18 + **Step 1: Add arrowUp to ICONS object** 19 + 20 + Add after line 8 (`back: 'fa-solid fa-arrow-left',`): 21 + 22 + ```javascript 23 + arrowUp: 'fa-solid fa-arrow-up', 24 + ``` 25 + 26 + **Step 2: Verify icon renders** 27 + 28 + Open app in browser, temporarily add `<grain-icon name="arrowUp"></grain-icon>` anywhere to confirm it renders. 29 + 30 + **Step 3: Commit** 31 + 32 + ```bash 33 + git add src/components/atoms/grain-icon.js 34 + git commit -m "feat: add arrowUp icon" 35 + ``` 36 + 37 + --- 38 + 39 + ### Task 2: Create grain-scroll-to-top component 40 + 41 + **Files:** 42 + - Create: `src/components/atoms/grain-scroll-to-top.js` 43 + 44 + **Step 1: Create the component file** 45 + 46 + ```javascript 47 + import { LitElement, html, css } from 'lit'; 48 + import './grain-icon.js'; 49 + 50 + export class GrainScrollToTop extends LitElement { 51 + static properties = { 52 + visible: { type: Boolean } 53 + }; 54 + 55 + static styles = css` 56 + :host { 57 + position: fixed; 58 + bottom: 20px; 59 + left: 20px; 60 + z-index: 100; 61 + } 62 + button { 63 + display: flex; 64 + align-items: center; 65 + justify-content: center; 66 + width: 48px; 67 + height: 48px; 68 + border-radius: 50%; 69 + border: 1px solid var(--color-border); 70 + background: var(--color-surface-secondary); 71 + color: var(--color-accent); 72 + cursor: pointer; 73 + opacity: 0; 74 + pointer-events: none; 75 + transition: opacity 0.2s ease-in-out; 76 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); 77 + } 78 + button.visible { 79 + opacity: 1; 80 + pointer-events: auto; 81 + } 82 + button:hover { 83 + filter: brightness(1.1); 84 + } 85 + button:active { 86 + transform: scale(0.95); 87 + } 88 + `; 89 + 90 + constructor() { 91 + super(); 92 + this.visible = false; 93 + } 94 + 95 + #handleClick() { 96 + this.dispatchEvent(new CustomEvent('scroll-top', { 97 + bubbles: true, 98 + composed: true 99 + })); 100 + } 101 + 102 + render() { 103 + return html` 104 + <button 105 + class=${this.visible ? 'visible' : ''} 106 + @click=${this.#handleClick} 107 + aria-label="Scroll to top" 108 + > 109 + <grain-icon name="arrowUp" size="20"></grain-icon> 110 + </button> 111 + `; 112 + } 113 + } 114 + 115 + customElements.define('grain-scroll-to-top', GrainScrollToTop); 116 + ``` 117 + 118 + **Step 2: Commit** 119 + 120 + ```bash 121 + git add src/components/atoms/grain-scroll-to-top.js 122 + git commit -m "feat: add grain-scroll-to-top component" 123 + ``` 124 + 125 + --- 126 + 127 + ### Task 3: Add scroll tracking to grain-timeline 128 + 129 + **Files:** 130 + - Modify: `src/components/pages/grain-timeline.js` 131 + 132 + **Step 1: Add state property for scroll button visibility** 133 + 134 + Add to static properties (line 13-24): 135 + 136 + ```javascript 137 + _showScrollTop: { state: true }, 138 + ``` 139 + 140 + **Step 2: Initialize state in constructor** 141 + 142 + Add after line 59 (`this._focusPhotoUrl = null;`): 143 + 144 + ```javascript 145 + this._showScrollTop = false; 146 + ``` 147 + 148 + **Step 3: Add scroll listener setup/teardown** 149 + 150 + Add bound handler property after line 46 (`#initialized = false;`): 151 + 152 + ```javascript 153 + #boundHandleScroll = null; 154 + ``` 155 + 156 + Add to connectedCallback (after line 88): 157 + 158 + ```javascript 159 + this.#boundHandleScroll = this.#handleScroll.bind(this); 160 + window.addEventListener('scroll', this.#boundHandleScroll, { passive: true }); 161 + ``` 162 + 163 + Add to disconnectedCallback (after line 93): 164 + 165 + ```javascript 166 + if (this.#boundHandleScroll) { 167 + window.removeEventListener('scroll', this.#boundHandleScroll); 168 + } 169 + ``` 170 + 171 + **Step 4: Add scroll handler method** 172 + 173 + Add after `#handleCommentSheetClose()` method (after line 194): 174 + 175 + ```javascript 176 + #handleScroll() { 177 + this._showScrollTop = window.scrollY > 150; 178 + } 179 + ``` 180 + 181 + **Step 5: Commit** 182 + 183 + ```bash 184 + git add src/components/pages/grain-timeline.js 185 + git commit -m "feat: add scroll position tracking to timeline" 186 + ``` 187 + 188 + --- 189 + 190 + ### Task 4: Add scroll-to-top button and handler to timeline 191 + 192 + **Files:** 193 + - Modify: `src/components/pages/grain-timeline.js` 194 + 195 + **Step 1: Import the component** 196 + 197 + Add after line 10 (`import '../atoms/grain-spinner.js';`): 198 + 199 + ```javascript 200 + import '../atoms/grain-scroll-to-top.js'; 201 + ``` 202 + 203 + **Step 2: Add click handler method** 204 + 205 + Add after `#handleScroll()` method: 206 + 207 + ```javascript 208 + async #handleScrollTop() { 209 + if (this._refreshing) return; 210 + 211 + window.scrollTo({ top: 0, behavior: 'smooth' }); 212 + 213 + // Wait for scroll to complete before refreshing 214 + await new Promise(resolve => setTimeout(resolve, 400)); 215 + 216 + await this.#handleRefresh(); 217 + } 218 + ``` 219 + 220 + **Step 3: Add component to render** 221 + 222 + Add after the closing `</grain-feed-layout>` tag (before line 229's closing backtick), outside the feed-layout: 223 + 224 + ```javascript 225 + <grain-scroll-to-top 226 + ?visible=${this._showScrollTop} 227 + @scroll-top=${this.#handleScrollTop} 228 + ></grain-scroll-to-top> 229 + ``` 230 + 231 + **Step 4: Commit** 232 + 233 + ```bash 234 + git add src/components/pages/grain-timeline.js 235 + git commit -m "feat: integrate scroll-to-top button in timeline" 236 + ``` 237 + 238 + --- 239 + 240 + ### Task 5: Manual verification 241 + 242 + **Step 1: Test scroll appearance** 243 + 244 + 1. Open the app timeline 245 + 2. Scroll down past 150px 246 + 3. Verify button fades in at bottom-left 247 + 248 + **Step 2: Test click behavior** 249 + 250 + 1. Click the button 251 + 2. Verify smooth scroll to top 252 + 3. Verify feed refreshes (loading spinner appears briefly) 253 + 4. Verify button fades out when at top 254 + 255 + **Step 3: Test edge cases** 256 + 257 + 1. Scroll down, click button multiple times rapidly - should not double-refresh 258 + 2. Button should not appear when already at top 259 + 260 + **Step 4: Final commit if any fixes needed** 261 + 262 + ```bash 263 + git add -A 264 + git commit -m "fix: scroll-to-top refinements" 265 + ```