@eox/pages-theme-eox 1.7.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.
@@ -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>
@@ -0,0 +1,52 @@
1
+ import ImageGallery from "./ImageGallery.vue";
2
+
3
+ describe("<ImageGallery />", () => {
4
+ const imageList = [
5
+ { src: "/img1.jpg", alt: "Img 1", caption: "Caption 1" },
6
+ { src: "/img2.jpg", alt: "Img 2", caption: "Caption 2" },
7
+ { src: "/img3.jpg", alt: "Img 3", caption: "Caption 3" },
8
+ ];
9
+
10
+ it("renders grid layout when explicitly requested", () => {
11
+ cy.mount(ImageGallery, {
12
+ props: {
13
+ images: imageList,
14
+ layout: "grid",
15
+ columns: 3,
16
+ },
17
+ });
18
+
19
+ cy.get(".grid-container").should("have.class", "cols-3");
20
+ cy.get("img").should("have.length", 3);
21
+ cy.get("figcaption").should("have.length", 3);
22
+ cy.contains("Caption 1").should("be.visible");
23
+ });
24
+
25
+ it("renders gallery/lightbox layout by default", () => {
26
+ const stringImages = "/img1.jpg, /img2.jpg, /img3.jpg";
27
+ cy.mount(ImageGallery, {
28
+ props: {
29
+ images: stringImages,
30
+ },
31
+ });
32
+
33
+ // Check main featured image
34
+ cy.get(".featured-wrapper img").should("have.attr", "src", "/img1.jpg");
35
+
36
+ // Check 2 thumbnails below
37
+ cy.get(".cols-thumbnail img").should("have.length", 2);
38
+ cy.get(".cols-thumbnail img").eq(0).should("have.attr", "src", "/img2.jpg");
39
+
40
+ // Click main image to trigger lightbox
41
+ cy.get(".featured-wrapper").click();
42
+
43
+ // Lightbox should be open
44
+ cy.get(".lightbox-overlay").should("exist");
45
+ cy.get(".lightbox-image").should("have.attr", "src", "/img1.jpg");
46
+ cy.contains("1 / 3").should("exist");
47
+
48
+ // Close lightbox
49
+ cy.get("button[aria-label='Close Lightbox']").click({ force: true });
50
+ cy.get(".lightbox-overlay").should("not.exist");
51
+ });
52
+ });