+3
-1
src/components/atoms/grain-icon.js
+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
+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