WIP PWA for Grain

feat: add grain-scroll-to-top component

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

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

+69
+69
src/components/atoms/grain-scroll-to-top.js
··· 1 + import { LitElement, html, css } from 'lit'; 2 + import './grain-icon.js'; 3 + 4 + export class GrainScrollToTop extends LitElement { 5 + static properties = { 6 + visible: { type: Boolean } 7 + }; 8 + 9 + static styles = css` 10 + :host { 11 + position: fixed; 12 + bottom: 20px; 13 + left: 20px; 14 + z-index: 100; 15 + } 16 + button { 17 + display: flex; 18 + align-items: center; 19 + justify-content: center; 20 + width: 48px; 21 + height: 48px; 22 + border-radius: 50%; 23 + border: 1px solid var(--color-border); 24 + background: var(--color-surface-secondary); 25 + color: var(--color-accent); 26 + cursor: pointer; 27 + opacity: 0; 28 + pointer-events: none; 29 + transition: opacity 0.2s ease-in-out; 30 + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); 31 + } 32 + button.visible { 33 + opacity: 1; 34 + pointer-events: auto; 35 + } 36 + button:hover { 37 + filter: brightness(1.1); 38 + } 39 + button:active { 40 + transform: scale(0.95); 41 + } 42 + `; 43 + 44 + constructor() { 45 + super(); 46 + this.visible = false; 47 + } 48 + 49 + #handleClick() { 50 + this.dispatchEvent(new CustomEvent('scroll-top', { 51 + bubbles: true, 52 + composed: true 53 + })); 54 + } 55 + 56 + render() { 57 + return html` 58 + <button 59 + class=${this.visible ? 'visible' : ''} 60 + @click=${this.#handleClick} 61 + aria-label="Scroll to top" 62 + > 63 + <grain-icon name="arrowUp" size="20"></grain-icon> 64 + </button> 65 + `; 66 + } 67 + } 68 + 69 + customElements.define('grain-scroll-to-top', GrainScrollToTop);