@eox/pages-theme-eox 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +70 -0
- package/package.json +1 -1
- package/src/components/ImageCarousel.cy.js +22 -0
- package/src/components/ImageCarousel.vue +268 -0
- package/src/components/ImageElement.cy.js +46 -0
- package/src/components/ImageElement.vue +269 -0
- package/src/components/ImageGallery.cy.js +52 -0
- package/src/components/ImageGallery.vue +622 -0
- package/src/components/SingleTweet.cy.js +27 -0
- package/src/components/SingleTweet.vue +155 -0
- package/src/index.js +9 -0
- package/src/team.data.js +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
# [1.9.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.8.0...v1.9.0) (2026-06-11)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- new components (ImageCarousel, ImageElement, ImageGallery, SingleTweet) and automated loaders (team) ([8ce5f28](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/8ce5f289f06c1d4ebd7ff4871008f0dec31dabcc))
|
|
8
|
+
|
|
3
9
|
# [1.8.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.7.0...v1.8.0) (2026-06-08)
|
|
4
10
|
|
|
5
11
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -379,6 +379,76 @@ An image-overlay card where a background image fills the card and text is render
|
|
|
379
379
|
/>
|
|
380
380
|
```
|
|
381
381
|
|
|
382
|
+
#### Image Element
|
|
383
|
+
|
|
384
|
+
Displays standard images with optional caption and width, and supports interactive side-by-side comparison slider when a `compare` prop is provided.
|
|
385
|
+
|
|
386
|
+
```html
|
|
387
|
+
<!-- Standard Image -->
|
|
388
|
+
<ImageElement
|
|
389
|
+
src="/images/someimage.jpg"
|
|
390
|
+
caption="Standard view"
|
|
391
|
+
width="450px"
|
|
392
|
+
/>
|
|
393
|
+
|
|
394
|
+
<!-- Image Comparison Slider -->
|
|
395
|
+
<ImageElement
|
|
396
|
+
src="/images/eoxcloudless2024.jpg"
|
|
397
|
+
compare="/images/eoxcloudless2023.jpg"
|
|
398
|
+
caption="2024 vs. 2023 comparison"
|
|
399
|
+
/>
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
#### Image Gallery
|
|
403
|
+
|
|
404
|
+
A unified, responsive image grid and lightbox gallery. Supports both plain link/grid layouts and touch-friendly lightbox galleries.
|
|
405
|
+
|
|
406
|
+
##### Properties
|
|
407
|
+
|
|
408
|
+
| Prop | Type | Default | Description |
|
|
409
|
+
| :--------- | :-------------- | :---------- | :---------------------------------------------------------------------------------------------------- |
|
|
410
|
+
| `images` | `String\|Array` | — | Comma-separated image paths string, array of paths, or array of image objects with caption and links. |
|
|
411
|
+
| `columns` | `Number` | `2` | Number of columns (1, 2, 3, 4, 5). |
|
|
412
|
+
| `layout` | `String` | `"gallery"` | Layout mode: `"gallery"` (featured image + thumbnails) or `"grid"` (regular grid of images). |
|
|
413
|
+
| `lightbox` | `Boolean` | `true` | Enable fullscreen lightbox overlay when clicking an image. |
|
|
414
|
+
|
|
415
|
+
```html
|
|
416
|
+
<!-- Gallery with Lightbox -->
|
|
417
|
+
<ImageGallery images="/img1.jpg, /img2.jpg, /img3.jpg" layout="gallery" />
|
|
418
|
+
|
|
419
|
+
<!-- Direct Image Grid with Captions & Links -->
|
|
420
|
+
<ImageGallery
|
|
421
|
+
layout="grid"
|
|
422
|
+
:columns="3"
|
|
423
|
+
:images="[
|
|
424
|
+
{ src: '/img1.jpg', caption: 'Link to A', link: '/page-a' },
|
|
425
|
+
{ src: '/img2.jpg', caption: 'Only Caption' },
|
|
426
|
+
{ src: '/img3.jpg', caption: 'Link to B', link: '/page-b' }
|
|
427
|
+
]"
|
|
428
|
+
/>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### Image Carousel
|
|
432
|
+
|
|
433
|
+
A simple CSS-driven accessible image slideshow.
|
|
434
|
+
|
|
435
|
+
```html
|
|
436
|
+
<ImageCarousel
|
|
437
|
+
:images="[
|
|
438
|
+
{ src: '/slide1.jpg', caption: 'Slide 1 Caption' },
|
|
439
|
+
{ src: '/slide2.jpg', caption: 'Slide 2 Caption' }
|
|
440
|
+
]"
|
|
441
|
+
/>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Single Tweet
|
|
445
|
+
|
|
446
|
+
A privacy-respecting Twitter/X embed wrapper that checks Matomo cookie consent before rendering.
|
|
447
|
+
|
|
448
|
+
```html
|
|
449
|
+
<SingleTweet id="1276442045352226817" />
|
|
450
|
+
```
|
|
451
|
+
|
|
382
452
|
#### Feedback Form
|
|
383
453
|
|
|
384
454
|
**Feedback Form** for collecting user feedback can be enabled by configuring the theme:
|
package/package.json
CHANGED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import ImageCarousel from "./ImageCarousel.vue";
|
|
2
|
+
|
|
3
|
+
describe("<ImageCarousel />", () => {
|
|
4
|
+
const images = [
|
|
5
|
+
{ src: "/slide1.jpg", alt: "Slide 1", caption: "Caption 1" },
|
|
6
|
+
{ src: "/slide2.jpg", alt: "Slide 2", caption: "Caption 2" },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
it("renders slider images, captions, and buttons", () => {
|
|
10
|
+
cy.mount(ImageCarousel, {
|
|
11
|
+
props: {
|
|
12
|
+
images,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
cy.get("img").should("have.length", 2);
|
|
17
|
+
cy.get("img").eq(0).should("have.attr", "src", "/slide1.jpg");
|
|
18
|
+
|
|
19
|
+
// Indicators should exist
|
|
20
|
+
cy.get("button").should("have.length", 4); // 2 nav arrows + 2 indicators
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="image-carousel-container group">
|
|
3
|
+
<!-- Slider Container -->
|
|
4
|
+
<div
|
|
5
|
+
ref="container"
|
|
6
|
+
class="carousel-slider scrollbar-none"
|
|
7
|
+
@scroll="handleScroll"
|
|
8
|
+
>
|
|
9
|
+
<div
|
|
10
|
+
v-for="(img, idx) in normalizedImages"
|
|
11
|
+
:key="idx"
|
|
12
|
+
class="carousel-slide"
|
|
13
|
+
>
|
|
14
|
+
<img
|
|
15
|
+
:src="img.src"
|
|
16
|
+
:alt="img.alt || `Carousel image ${idx + 1}`"
|
|
17
|
+
class="carousel-img"
|
|
18
|
+
/>
|
|
19
|
+
<div
|
|
20
|
+
v-if="img.caption"
|
|
21
|
+
class="carousel-caption"
|
|
22
|
+
v-html="img.caption"
|
|
23
|
+
></div>
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<!-- Navigation Arrows -->
|
|
28
|
+
<button
|
|
29
|
+
v-if="normalizedImages.length > 1"
|
|
30
|
+
@click="prev"
|
|
31
|
+
class="carousel-nav carousel-prev"
|
|
32
|
+
aria-label="Previous Slide"
|
|
33
|
+
>
|
|
34
|
+
<svg
|
|
35
|
+
class="carousel-icon"
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
stroke-width="2"
|
|
40
|
+
>
|
|
41
|
+
<path
|
|
42
|
+
stroke-linecap="round"
|
|
43
|
+
stroke-linejoin="round"
|
|
44
|
+
d="M15 19l-7-7 7-7"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
</button>
|
|
48
|
+
|
|
49
|
+
<button
|
|
50
|
+
v-if="normalizedImages.length > 1"
|
|
51
|
+
@click="next"
|
|
52
|
+
class="carousel-nav carousel-next"
|
|
53
|
+
aria-label="Next Slide"
|
|
54
|
+
>
|
|
55
|
+
<svg
|
|
56
|
+
class="carousel-icon"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
stroke-width="2"
|
|
61
|
+
>
|
|
62
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
|
|
66
|
+
<!-- Indicators -->
|
|
67
|
+
<div v-if="normalizedImages.length > 1" class="carousel-indicators">
|
|
68
|
+
<button
|
|
69
|
+
v-for="(_, idx) in normalizedImages"
|
|
70
|
+
:key="idx"
|
|
71
|
+
@click="goTo(idx)"
|
|
72
|
+
class="carousel-dot"
|
|
73
|
+
:class="{ active: activeIndex === idx }"
|
|
74
|
+
:aria-label="`Go to slide ${idx + 1}`"
|
|
75
|
+
></button>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup>
|
|
81
|
+
import { computed, ref } from "vue";
|
|
82
|
+
|
|
83
|
+
const props = defineProps({
|
|
84
|
+
images: {
|
|
85
|
+
type: Array,
|
|
86
|
+
required: true,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const container = ref(null);
|
|
91
|
+
const activeIndex = ref(0);
|
|
92
|
+
|
|
93
|
+
const normalizedImages = computed(() => {
|
|
94
|
+
if (!props.images) return [];
|
|
95
|
+
return props.images.map((img) => {
|
|
96
|
+
if (typeof img === "string") {
|
|
97
|
+
return { src: img, alt: "", caption: "" };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
src: img.src || "",
|
|
101
|
+
alt: img.alt || "",
|
|
102
|
+
caption: img.caption || "",
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const handleScroll = () => {
|
|
108
|
+
if (!container.value) return;
|
|
109
|
+
const { scrollLeft, clientWidth } = container.value;
|
|
110
|
+
if (clientWidth === 0) return;
|
|
111
|
+
activeIndex.value = Math.round(scrollLeft / clientWidth);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const goTo = (index) => {
|
|
115
|
+
if (!container.value) return;
|
|
116
|
+
const width = container.value.clientWidth;
|
|
117
|
+
container.value.scrollTo({
|
|
118
|
+
left: width * index,
|
|
119
|
+
behavior: "smooth",
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const prev = () => {
|
|
124
|
+
const index =
|
|
125
|
+
(activeIndex.value - 1 + normalizedImages.value.length) %
|
|
126
|
+
normalizedImages.value.length;
|
|
127
|
+
goTo(index);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const next = () => {
|
|
131
|
+
const index = (activeIndex.value + 1) % normalizedImages.value.length;
|
|
132
|
+
goTo(index);
|
|
133
|
+
};
|
|
134
|
+
</script>
|
|
135
|
+
|
|
136
|
+
<style scoped>
|
|
137
|
+
.image-carousel-container {
|
|
138
|
+
position: relative;
|
|
139
|
+
width: 100%;
|
|
140
|
+
margin: 1.5rem 0;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
border-radius: 8px;
|
|
143
|
+
border: 1px solid #e2e8f0;
|
|
144
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
145
|
+
background-color: #f8fafc;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.carousel-slider {
|
|
149
|
+
display: flex;
|
|
150
|
+
overflow-x: auto;
|
|
151
|
+
scroll-snap-type: x mandatory;
|
|
152
|
+
scroll-behavior: smooth;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Hide scrollbars */
|
|
156
|
+
.scrollbar-none::-webkit-scrollbar {
|
|
157
|
+
display: none;
|
|
158
|
+
}
|
|
159
|
+
.scrollbar-none {
|
|
160
|
+
-ms-overflow-style: none;
|
|
161
|
+
scrollbar-width: none;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.carousel-slide {
|
|
165
|
+
width: 100%;
|
|
166
|
+
flex-shrink: 0;
|
|
167
|
+
scroll-snap-align: start;
|
|
168
|
+
scroll-snap-stop: always;
|
|
169
|
+
position: relative;
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
align-items: center;
|
|
173
|
+
justify-content: center;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.carousel-img {
|
|
177
|
+
width: 100%;
|
|
178
|
+
max-height: 500px;
|
|
179
|
+
object-fit: cover;
|
|
180
|
+
display: block;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.carousel-caption {
|
|
184
|
+
position: absolute;
|
|
185
|
+
bottom: 0;
|
|
186
|
+
left: 0;
|
|
187
|
+
right: 0;
|
|
188
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
189
|
+
color: #ffffff;
|
|
190
|
+
font-size: 0.875rem;
|
|
191
|
+
padding: 0.75rem 1.5rem;
|
|
192
|
+
text-align: center;
|
|
193
|
+
backdrop-filter: blur(4px);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.carousel-nav {
|
|
197
|
+
position: absolute;
|
|
198
|
+
top: 50%;
|
|
199
|
+
transform: translateY(-50%);
|
|
200
|
+
width: 40px;
|
|
201
|
+
height: 40px;
|
|
202
|
+
border-radius: 50%;
|
|
203
|
+
background-color: rgba(0, 0, 0, 0.4);
|
|
204
|
+
border: none;
|
|
205
|
+
color: #ffffff;
|
|
206
|
+
display: flex;
|
|
207
|
+
align-items: center;
|
|
208
|
+
justify-content: center;
|
|
209
|
+
cursor: pointer !important;
|
|
210
|
+
opacity: 0;
|
|
211
|
+
transition:
|
|
212
|
+
opacity 0.2s ease-in-out,
|
|
213
|
+
background-color 0.2s;
|
|
214
|
+
z-index: 10;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.carousel-nav:hover {
|
|
218
|
+
background-color: rgba(0, 0, 0, 0.6);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.image-carousel-container:hover .carousel-nav {
|
|
222
|
+
opacity: 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.carousel-prev {
|
|
226
|
+
left: 1rem;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.carousel-next {
|
|
230
|
+
right: 1rem;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.carousel-icon {
|
|
234
|
+
width: 24px;
|
|
235
|
+
height: 24px;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.carousel-indicators {
|
|
239
|
+
position: absolute;
|
|
240
|
+
bottom: 1rem;
|
|
241
|
+
left: 50%;
|
|
242
|
+
transform: translateX(-50%);
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: 0.5rem;
|
|
245
|
+
z-index: 10;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.carousel-dot {
|
|
249
|
+
width: 10px;
|
|
250
|
+
height: 10px;
|
|
251
|
+
border-radius: 50%;
|
|
252
|
+
border: none;
|
|
253
|
+
background-color: rgba(255, 255, 255, 0.5);
|
|
254
|
+
cursor: pointer !important;
|
|
255
|
+
transition: all 0.3s;
|
|
256
|
+
padding: 0;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.carousel-dot:hover {
|
|
260
|
+
background-color: rgba(255, 255, 255, 0.85);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.carousel-dot.active {
|
|
264
|
+
background-color: #ffffff;
|
|
265
|
+
width: 20px;
|
|
266
|
+
border-radius: 5px;
|
|
267
|
+
}
|
|
268
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import ImageElement from "./ImageElement.vue";
|
|
2
|
+
|
|
3
|
+
describe("<ImageElement />", () => {
|
|
4
|
+
it("renders standard image with caption and custom width", () => {
|
|
5
|
+
const src = "/images/someimage.png";
|
|
6
|
+
const caption = "Nice view of the <em>Earth</em>";
|
|
7
|
+
const width = "300px";
|
|
8
|
+
|
|
9
|
+
cy.mount(ImageElement, {
|
|
10
|
+
props: {
|
|
11
|
+
src,
|
|
12
|
+
caption,
|
|
13
|
+
width,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
cy.get("img")
|
|
18
|
+
.should("have.attr", "src", src)
|
|
19
|
+
.should("have.css", "max-width", width);
|
|
20
|
+
|
|
21
|
+
cy.get("figcaption").should(
|
|
22
|
+
"contain.html",
|
|
23
|
+
"Nice view of the <em>Earth</em>",
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders compare slider when compare prop is provided", () => {
|
|
28
|
+
const src = "/images/2024.jpg";
|
|
29
|
+
const compare = "/images/2023.jpg";
|
|
30
|
+
|
|
31
|
+
cy.mount(ImageElement, {
|
|
32
|
+
props: {
|
|
33
|
+
src,
|
|
34
|
+
compare,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Check that two images are rendered
|
|
39
|
+
cy.get("img").should("have.length", 2);
|
|
40
|
+
cy.get("img").eq(0).should("have.attr", "src", compare);
|
|
41
|
+
cy.get("img").eq(1).should("have.attr", "src", src);
|
|
42
|
+
|
|
43
|
+
// Check slider element exists
|
|
44
|
+
cy.get('input[type="range"]').should("exist").and("have.value", "50");
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="image-element-container">
|
|
3
|
+
<figure class="image-element-figure">
|
|
4
|
+
<!-- Image Compare Mode -->
|
|
5
|
+
<div
|
|
6
|
+
v-if="compare"
|
|
7
|
+
class="compare-wrapper"
|
|
8
|
+
:style="{ maxWidth: width || '100%' }"
|
|
9
|
+
>
|
|
10
|
+
<!-- Background Image (Original/Right) -->
|
|
11
|
+
<img
|
|
12
|
+
:src="compare"
|
|
13
|
+
:alt="alt || 'Image comparison background'"
|
|
14
|
+
class="compare-img compare-bg"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<!-- Foreground Image (Overlay/Left) -->
|
|
18
|
+
<div
|
|
19
|
+
class="compare-overlay"
|
|
20
|
+
:style="{
|
|
21
|
+
clipPath: `polygon(0 0, ${sliderValue}% 0, ${sliderValue}% 100%, 0 100%)`,
|
|
22
|
+
}"
|
|
23
|
+
>
|
|
24
|
+
<img
|
|
25
|
+
:src="src"
|
|
26
|
+
:alt="alt || 'Image comparison foreground'"
|
|
27
|
+
class="compare-img compare-fg"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<!-- Slider Line and Handle -->
|
|
32
|
+
<div class="compare-handle-line" :style="{ left: `${sliderValue}%` }">
|
|
33
|
+
<div class="compare-handle-circle">
|
|
34
|
+
<svg
|
|
35
|
+
class="compare-handle-icon"
|
|
36
|
+
viewBox="0 0 24 24"
|
|
37
|
+
fill="none"
|
|
38
|
+
stroke="currentColor"
|
|
39
|
+
stroke-width="2"
|
|
40
|
+
>
|
|
41
|
+
<path
|
|
42
|
+
stroke-linecap="round"
|
|
43
|
+
stroke-linejoin="round"
|
|
44
|
+
d="M8 9l-3 3 3 3m8-6l3 3-3 3"
|
|
45
|
+
/>
|
|
46
|
+
</svg>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Range Input Overlay (covers full container to handle drag smoothly) -->
|
|
51
|
+
<input
|
|
52
|
+
type="range"
|
|
53
|
+
min="0"
|
|
54
|
+
max="100"
|
|
55
|
+
v-model="sliderValue"
|
|
56
|
+
class="compare-range-input"
|
|
57
|
+
aria-label="Image comparison slider"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<!-- Standard Image Mode -->
|
|
62
|
+
<img
|
|
63
|
+
v-else
|
|
64
|
+
:src="src"
|
|
65
|
+
:alt="alt || 'Post image'"
|
|
66
|
+
:style="{ maxWidth: width || '100%' }"
|
|
67
|
+
class="standard-img"
|
|
68
|
+
/>
|
|
69
|
+
|
|
70
|
+
<!-- Caption -->
|
|
71
|
+
<figcaption
|
|
72
|
+
v-if="caption"
|
|
73
|
+
class="image-caption"
|
|
74
|
+
v-html="caption"
|
|
75
|
+
></figcaption>
|
|
76
|
+
</figure>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup>
|
|
81
|
+
import { ref } from "vue";
|
|
82
|
+
|
|
83
|
+
const props = defineProps({
|
|
84
|
+
src: {
|
|
85
|
+
type: String,
|
|
86
|
+
required: true,
|
|
87
|
+
},
|
|
88
|
+
compare: {
|
|
89
|
+
type: String,
|
|
90
|
+
default: "",
|
|
91
|
+
},
|
|
92
|
+
alt: {
|
|
93
|
+
type: String,
|
|
94
|
+
default: "",
|
|
95
|
+
},
|
|
96
|
+
width: {
|
|
97
|
+
type: String,
|
|
98
|
+
default: "",
|
|
99
|
+
},
|
|
100
|
+
caption: {
|
|
101
|
+
type: String,
|
|
102
|
+
default: "",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const sliderValue = ref(50);
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<style scoped>
|
|
110
|
+
.image-element-container {
|
|
111
|
+
display: flex;
|
|
112
|
+
flex-direction: column;
|
|
113
|
+
align-items: center;
|
|
114
|
+
justify-content: center;
|
|
115
|
+
margin: 1.5rem 0;
|
|
116
|
+
width: 100%;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.image-element-figure {
|
|
120
|
+
width: 100%;
|
|
121
|
+
display: flex;
|
|
122
|
+
flex-direction: column;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
text-align: center;
|
|
126
|
+
margin: 0.5rem auto;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.compare-wrapper {
|
|
130
|
+
position: relative;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
user-select: none;
|
|
133
|
+
width: 100%;
|
|
134
|
+
border-radius: 8px;
|
|
135
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
136
|
+
border: 1px solid #e2e8f0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.compare-img {
|
|
140
|
+
width: 100%;
|
|
141
|
+
height: auto;
|
|
142
|
+
display: block;
|
|
143
|
+
object-fit: cover;
|
|
144
|
+
pointer-events: none;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.compare-bg {
|
|
148
|
+
/* base background */
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.compare-overlay {
|
|
152
|
+
position: absolute;
|
|
153
|
+
top: 0;
|
|
154
|
+
left: 0;
|
|
155
|
+
right: 0;
|
|
156
|
+
bottom: 0;
|
|
157
|
+
overflow: hidden;
|
|
158
|
+
pointer-events: none;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.compare-fg {
|
|
162
|
+
position: absolute;
|
|
163
|
+
top: 0;
|
|
164
|
+
left: 0;
|
|
165
|
+
width: 100%;
|
|
166
|
+
height: 100%;
|
|
167
|
+
max-width: none;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.compare-handle-line {
|
|
171
|
+
position: absolute;
|
|
172
|
+
top: 0;
|
|
173
|
+
bottom: 0;
|
|
174
|
+
width: 2px;
|
|
175
|
+
background-color: #ffffff;
|
|
176
|
+
cursor: ew-resize;
|
|
177
|
+
pointer-events: none;
|
|
178
|
+
z-index: 10;
|
|
179
|
+
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.compare-handle-circle {
|
|
183
|
+
position: absolute;
|
|
184
|
+
top: 50%;
|
|
185
|
+
left: 50%;
|
|
186
|
+
transform: translate(-50%, -50%);
|
|
187
|
+
width: 32px;
|
|
188
|
+
height: 32px;
|
|
189
|
+
border-radius: 50%;
|
|
190
|
+
background-color: #ffffff;
|
|
191
|
+
border: 1px solid #cbd5e1;
|
|
192
|
+
display: flex;
|
|
193
|
+
align-items: center;
|
|
194
|
+
justify-content: center;
|
|
195
|
+
box-shadow:
|
|
196
|
+
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
197
|
+
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
.compare-handle-icon {
|
|
201
|
+
width: 20px;
|
|
202
|
+
height: 20px;
|
|
203
|
+
color: #64748b;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Range Input Overlay to handle drag */
|
|
207
|
+
.compare-range-input {
|
|
208
|
+
position: absolute;
|
|
209
|
+
top: 0;
|
|
210
|
+
left: 0;
|
|
211
|
+
width: 100%;
|
|
212
|
+
height: 100%;
|
|
213
|
+
opacity: 0;
|
|
214
|
+
cursor: ew-resize;
|
|
215
|
+
z-index: 20;
|
|
216
|
+
margin: 0;
|
|
217
|
+
-webkit-appearance: none;
|
|
218
|
+
appearance: none;
|
|
219
|
+
background: transparent;
|
|
220
|
+
border: none;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.compare-range-input::-webkit-slider-runnable-track {
|
|
224
|
+
background: transparent;
|
|
225
|
+
border: none;
|
|
226
|
+
height: 100%;
|
|
227
|
+
width: 100%;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.compare-range-input::-webkit-slider-thumb {
|
|
231
|
+
-webkit-appearance: none;
|
|
232
|
+
appearance: none;
|
|
233
|
+
height: 100%;
|
|
234
|
+
width: 40px;
|
|
235
|
+
background: transparent;
|
|
236
|
+
cursor: ew-resize;
|
|
237
|
+
margin-top: 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.compare-range-input::-moz-range-track {
|
|
241
|
+
background: transparent;
|
|
242
|
+
border: none;
|
|
243
|
+
height: 100%;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.compare-range-input::-moz-range-thumb {
|
|
247
|
+
height: 100%;
|
|
248
|
+
width: 40px;
|
|
249
|
+
background: transparent;
|
|
250
|
+
cursor: ew-resize;
|
|
251
|
+
border: none;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.standard-img {
|
|
255
|
+
border-radius: 8px;
|
|
256
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
257
|
+
display: block;
|
|
258
|
+
height: auto;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.image-caption {
|
|
262
|
+
font-size: 0.85rem;
|
|
263
|
+
margin-top: 0.75rem;
|
|
264
|
+
text-align: center;
|
|
265
|
+
color: #64748b;
|
|
266
|
+
max-width: 40rem;
|
|
267
|
+
line-height: 1.5;
|
|
268
|
+
}
|
|
269
|
+
</style>
|