feat: add navigation arrows to image carousel

Add left/right navigation arrows to the image carousel for desktop users:
- Arrows appear on multi-image galleries only
- Left arrow hidden on first image, right hidden on last
- White semi-transparent background with muted brown chevron icons
- Uses grain-icon component for consistent iconography
- Click events don't propagate to parent (prevents gallery navigation)

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

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

Changed files
+299 -3
docs
src
components
+3 -1
src/components/atoms/grain-icon.js
··· 23 23 share: 'fa-solid fa-arrow-up-from-bracket', 24 24 camera: 'fa-solid fa-camera', 25 25 paperPlane: 'fa-regular fa-paper-plane', 26 - close: 'fa-solid fa-xmark' 26 + close: 'fa-solid fa-xmark', 27 + chevronLeft: 'fa-solid fa-chevron-left', 28 + chevronRight: 'fa-solid fa-chevron-right' 27 29 }; 28 30 29 31 export class GrainIcon extends LitElement {
+73 -2
src/components/organisms/grain-image-carousel.js
··· 1 1 import { LitElement, html, css } from 'lit'; 2 2 import '../atoms/grain-image.js'; 3 + import '../atoms/grain-icon.js'; 3 4 import '../molecules/grain-carousel-dots.js'; 4 5 5 6 export class GrainImageCarousel extends LitElement { ··· 42 43 left: 0; 43 44 right: 0; 44 45 } 46 + .nav-arrow { 47 + position: absolute; 48 + top: 50%; 49 + transform: translateY(-50%); 50 + width: 24px; 51 + height: 24px; 52 + border-radius: 50%; 53 + border: none; 54 + background: rgba(255, 255, 255, 0.7); 55 + color: rgba(120, 100, 90, 1); 56 + cursor: pointer; 57 + display: flex; 58 + align-items: center; 59 + justify-content: center; 60 + padding: 0; 61 + z-index: 1; 62 + } 63 + .nav-arrow:hover { 64 + background: rgba(255, 255, 255, 1); 65 + } 66 + .nav-arrow:focus { 67 + outline: none; 68 + } 69 + .nav-arrow:focus-visible { 70 + outline: 2px solid rgba(120, 100, 90, 0.5); 71 + outline-offset: 2px; 72 + } 73 + .nav-arrow-left { 74 + left: 8px; 75 + } 76 + .nav-arrow-right { 77 + right: 8px; 78 + } 45 79 `; 46 80 47 81 constructor() { ··· 67 101 } 68 102 } 69 103 104 + #goToPrevious(e) { 105 + e.stopPropagation(); 106 + if (this._currentIndex > 0) { 107 + const carousel = this.shadowRoot.querySelector('.carousel'); 108 + const slides = carousel.querySelectorAll('.slide'); 109 + slides[this._currentIndex - 1].scrollIntoView({ 110 + behavior: 'smooth', 111 + block: 'nearest', 112 + inline: 'start' 113 + }); 114 + } 115 + } 116 + 117 + #goToNext(e) { 118 + e.stopPropagation(); 119 + if (this._currentIndex < this.photos.length - 1) { 120 + const carousel = this.shadowRoot.querySelector('.carousel'); 121 + const slides = carousel.querySelectorAll('.slide'); 122 + slides[this._currentIndex + 1].scrollIntoView({ 123 + behavior: 'smooth', 124 + block: 'nearest', 125 + inline: 'start' 126 + }); 127 + } 128 + } 129 + 70 130 #shouldLoad(index) { 71 131 // Load current slide and 1 slide ahead/behind for smooth swiping 72 132 return Math.abs(index - this._currentIndex) <= 1; ··· 79 139 render() { 80 140 const hasPortrait = this.#hasPortrait; 81 141 const minAspectRatio = this.#minAspectRatio; 82 - 83 - // Calculate height based on tallest image when portrait exists 84 142 const carouselStyle = hasPortrait 85 143 ? `aspect-ratio: ${minAspectRatio};` 86 144 : ''; 87 145 146 + const showLeftArrow = this.photos.length > 1 && this._currentIndex > 0; 147 + const showRightArrow = this.photos.length > 1 && this._currentIndex < this.photos.length - 1; 148 + 88 149 return html` 89 150 <div class="carousel" style=${carouselStyle} @scroll=${this.#handleScroll}> 90 151 ${this.photos.map((photo, index) => html` ··· 98 159 </div> 99 160 `)} 100 161 </div> 162 + ${showLeftArrow ? html` 163 + <button class="nav-arrow nav-arrow-left" @click=${this.#goToPrevious} aria-label="Previous image"> 164 + <grain-icon name="chevronLeft" size="12"></grain-icon> 165 + </button> 166 + ` : ''} 167 + ${showRightArrow ? html` 168 + <button class="nav-arrow nav-arrow-right" @click=${this.#goToNext} aria-label="Next image"> 169 + <grain-icon name="chevronRight" size="12"></grain-icon> 170 + </button> 171 + ` : ''} 101 172 ${this.photos.length > 1 ? html` 102 173 <div class="dots"> 103 174 <grain-carousel-dots