@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.
@@ -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
+ });
@@ -0,0 +1,622 @@
1
+ <template>
2
+ <div class="image-gallery-container">
3
+ <!-- 1. Grid Layout (Legacy GalleryGrid) -->
4
+ <div v-if="layoutMode === 'grid'" class="grid-container" :class="colsClass">
5
+ <div v-for="(img, idx) in normalizedImages" :key="idx" class="grid-item">
6
+ <figure class="grid-figure">
7
+ <!-- Wrap in Link if present -->
8
+ <a v-if="img.link" :href="img.link" class="grid-link">
9
+ <img
10
+ :src="img.src"
11
+ :alt="img.alt || 'Gallery image'"
12
+ :style="{ maxWidth: img.width || '100%' }"
13
+ class="grid-img"
14
+ />
15
+ <figcaption
16
+ v-if="img.caption"
17
+ class="grid-caption"
18
+ v-html="img.caption"
19
+ ></figcaption>
20
+ </a>
21
+ <!-- No Link -->
22
+ <template v-else>
23
+ <img
24
+ :src="img.src"
25
+ :alt="img.alt || 'Gallery image'"
26
+ :style="{ maxWidth: img.width || '100%' }"
27
+ class="grid-img"
28
+ :class="{ clickable: lightbox }"
29
+ @click="lightbox ? openLightbox(idx) : null"
30
+ />
31
+ <figcaption
32
+ v-if="img.caption"
33
+ class="grid-caption"
34
+ v-html="img.caption"
35
+ ></figcaption>
36
+ </template>
37
+ </figure>
38
+ </div>
39
+ </div>
40
+
41
+ <!-- 2. Gallery Layout with Featured/Thumbnails (eoxcloudless style) -->
42
+ <div v-else class="gallery-layout-wrapper">
43
+ <!-- Main Featured Image -->
44
+ <div
45
+ v-if="normalizedImages.length > 0"
46
+ class="featured-wrapper clickable"
47
+ @click="openLightbox(0)"
48
+ >
49
+ <img
50
+ :src="normalizedImages[0].src"
51
+ :alt="normalizedImages[0].alt || 'Featured gallery image'"
52
+ class="featured-img"
53
+ />
54
+ </div>
55
+
56
+ <!-- Thumbnail Grid -->
57
+ <div
58
+ v-if="normalizedImages.length > 1"
59
+ class="grid-container cols-thumbnail mt-4"
60
+ >
61
+ <div
62
+ v-for="(img, idx) in normalizedImages.slice(1)"
63
+ :key="idx"
64
+ class="thumbnail-item clickable"
65
+ @click="openLightbox(idx + 1)"
66
+ >
67
+ <img
68
+ :src="img.src"
69
+ :alt="img.alt || 'Gallery thumbnail'"
70
+ class="thumbnail-img"
71
+ />
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Fullscreen Lightbox Modal -->
77
+ <Transition name="fade">
78
+ <div
79
+ v-if="lightboxIndex !== null"
80
+ class="lightbox-overlay"
81
+ @click.self="closeLightbox"
82
+ >
83
+ <!-- Header (Close + Counter) -->
84
+ <div class="lightbox-header">
85
+ <span class="lightbox-counter">
86
+ {{ lightboxIndex + 1 }} / {{ normalizedImages.length }}
87
+ </span>
88
+ <button
89
+ @click="closeLightbox"
90
+ class="lightbox-btn lightbox-close"
91
+ aria-label="Close Lightbox"
92
+ >
93
+ <svg
94
+ class="lightbox-icon"
95
+ viewBox="0 0 24 24"
96
+ fill="none"
97
+ stroke="currentColor"
98
+ stroke-width="2"
99
+ >
100
+ <path
101
+ stroke-linecap="round"
102
+ stroke-linejoin="round"
103
+ d="M6 18L18 6M6 6l12 12"
104
+ />
105
+ </svg>
106
+ </button>
107
+ </div>
108
+
109
+ <!-- Previous Button -->
110
+ <button
111
+ v-if="normalizedImages.length > 1"
112
+ @click="prevImage"
113
+ class="lightbox-btn lightbox-nav lightbox-prev"
114
+ aria-label="Previous Image"
115
+ >
116
+ <svg
117
+ class="lightbox-nav-icon"
118
+ viewBox="0 0 24 24"
119
+ fill="none"
120
+ stroke="currentColor"
121
+ stroke-width="2"
122
+ >
123
+ <path
124
+ stroke-linecap="round"
125
+ stroke-linejoin="round"
126
+ d="M15 19l-7-7 7-7"
127
+ />
128
+ </svg>
129
+ </button>
130
+
131
+ <!-- Next Button -->
132
+ <button
133
+ v-if="normalizedImages.length > 1"
134
+ @click="nextImage"
135
+ class="lightbox-btn lightbox-nav lightbox-next"
136
+ aria-label="Next Image"
137
+ >
138
+ <svg
139
+ class="lightbox-nav-icon"
140
+ viewBox="0 0 24 24"
141
+ fill="none"
142
+ stroke="currentColor"
143
+ stroke-width="2"
144
+ >
145
+ <path
146
+ stroke-linecap="round"
147
+ stroke-linejoin="round"
148
+ d="M9 5l7 7-7 7"
149
+ />
150
+ </svg>
151
+ </button>
152
+
153
+ <!-- Lightbox Content -->
154
+ <div class="lightbox-content" @click.self="closeLightbox">
155
+ <img
156
+ :src="normalizedImages[lightboxIndex].src"
157
+ class="lightbox-image"
158
+ :alt="normalizedImages[lightboxIndex].alt || 'Expanded view'"
159
+ />
160
+ <p
161
+ v-if="normalizedImages[lightboxIndex].caption"
162
+ class="lightbox-caption"
163
+ v-html="normalizedImages[lightboxIndex].caption"
164
+ ></p>
165
+ </div>
166
+ </div>
167
+ </Transition>
168
+ </div>
169
+ </template>
170
+
171
+ <script setup>
172
+ import { computed, ref, onMounted, onUnmounted } from "vue";
173
+
174
+ const props = defineProps({
175
+ images: {
176
+ type: [String, Array],
177
+ required: true,
178
+ },
179
+ columns: {
180
+ type: Number,
181
+ default: 2,
182
+ },
183
+ layout: {
184
+ type: String,
185
+ default: "gallery", // 'gallery' (lightbox) or 'grid' (GalleryGrid)
186
+ },
187
+ lightbox: {
188
+ type: Boolean,
189
+ default: true,
190
+ },
191
+ });
192
+
193
+ const lightboxIndex = ref(null);
194
+
195
+ const layoutMode = computed(() => {
196
+ // If explicitly 'grid', or if we detect link in image objects, default to grid
197
+ if (props.layout === "grid") return "grid";
198
+ if (
199
+ Array.isArray(props.images) &&
200
+ props.images.some(
201
+ (img) => typeof img === "object" && (img.link || img.caption),
202
+ )
203
+ ) {
204
+ return "grid";
205
+ }
206
+ return "gallery";
207
+ });
208
+
209
+ const normalizedImages = computed(() => {
210
+ if (!props.images) return [];
211
+ if (Array.isArray(props.images)) {
212
+ return props.images.map((img) => {
213
+ if (typeof img === "string") {
214
+ return { src: img, alt: "", caption: "", link: "", width: "" };
215
+ }
216
+ return {
217
+ src: img.src || "",
218
+ alt: img.alt || "",
219
+ caption: img.caption || "",
220
+ link: img.link || "",
221
+ width: img.width || "",
222
+ };
223
+ });
224
+ }
225
+ if (typeof props.images === "string") {
226
+ return props.images
227
+ .split(",")
228
+ .map((img) => img.trim())
229
+ .filter(Boolean)
230
+ .map((src) => ({ src, alt: "", caption: "", link: "", width: "" }));
231
+ }
232
+ return [];
233
+ });
234
+
235
+ const colsClass = computed(() => {
236
+ const cols = props.columns;
237
+ if (cols === 1) return "cols-1";
238
+ if (cols === 3) return "cols-3";
239
+ if (cols === 4) return "cols-4";
240
+ if (cols === 5) return "cols-5";
241
+ return "cols-2"; // default
242
+ });
243
+
244
+ const openLightbox = (index) => {
245
+ if (!props.lightbox) return;
246
+ lightboxIndex.value = index;
247
+ if (typeof document !== "undefined") {
248
+ document.body.style.overflow = "hidden";
249
+ }
250
+ };
251
+
252
+ const closeLightbox = () => {
253
+ lightboxIndex.value = null;
254
+ if (typeof document !== "undefined") {
255
+ document.body.style.overflow = "";
256
+ }
257
+ };
258
+
259
+ const prevImage = () => {
260
+ if (normalizedImages.value.length <= 1) return;
261
+ lightboxIndex.value =
262
+ (lightboxIndex.value - 1 + normalizedImages.value.length) %
263
+ normalizedImages.value.length;
264
+ };
265
+
266
+ const nextImage = () => {
267
+ if (normalizedImages.value.length <= 1) return;
268
+ lightboxIndex.value =
269
+ (lightboxIndex.value + 1) % normalizedImages.value.length;
270
+ };
271
+
272
+ const handleKeydown = (e) => {
273
+ if (lightboxIndex.value === null) return;
274
+ if (e.key === "ArrowRight" || e.key === "Right") {
275
+ nextImage();
276
+ } else if (e.key === "ArrowLeft" || e.key === "Left") {
277
+ prevImage();
278
+ } else if (e.key === "Escape" || e.key === "Esc") {
279
+ closeLightbox();
280
+ }
281
+ };
282
+
283
+ onMounted(() => {
284
+ window.addEventListener("keydown", handleKeydown);
285
+ });
286
+
287
+ onUnmounted(() => {
288
+ window.removeEventListener("keydown", handleKeydown);
289
+ if (typeof document !== "undefined") {
290
+ document.body.style.overflow = "";
291
+ }
292
+ });
293
+ </script>
294
+
295
+ <style scoped>
296
+ .image-gallery-container {
297
+ width: 100%;
298
+ margin: 1.5rem 0;
299
+ }
300
+
301
+ .grid-container {
302
+ display: grid;
303
+ gap: 1.5rem;
304
+ width: 100%;
305
+ }
306
+
307
+ .cols-1 {
308
+ grid-template-columns: repeat(1, minmax(0, 1fr));
309
+ }
310
+
311
+ .cols-2 {
312
+ grid-template-columns: repeat(1, minmax(0, 1fr));
313
+ }
314
+
315
+ .cols-3 {
316
+ grid-template-columns: repeat(1, minmax(0, 1fr));
317
+ }
318
+
319
+ .cols-4 {
320
+ grid-template-columns: repeat(2, minmax(0, 1fr));
321
+ }
322
+
323
+ .cols-5 {
324
+ grid-template-columns: repeat(2, minmax(0, 1fr));
325
+ }
326
+
327
+ @media (min-width: 640px) {
328
+ .cols-2 {
329
+ grid-template-columns: repeat(2, minmax(0, 1fr));
330
+ }
331
+ .cols-3 {
332
+ grid-template-columns: repeat(2, minmax(0, 1fr));
333
+ }
334
+ .cols-4 {
335
+ grid-template-columns: repeat(3, minmax(0, 1fr));
336
+ }
337
+ .cols-5 {
338
+ grid-template-columns: repeat(3, minmax(0, 1fr));
339
+ }
340
+ }
341
+
342
+ @media (min-width: 768px) {
343
+ .cols-3 {
344
+ grid-template-columns: repeat(3, minmax(0, 1fr));
345
+ }
346
+ .cols-4 {
347
+ grid-template-columns: repeat(4, minmax(0, 1fr));
348
+ }
349
+ .cols-5 {
350
+ grid-template-columns: repeat(5, minmax(0, 1fr));
351
+ }
352
+ }
353
+
354
+ .cols-thumbnail {
355
+ grid-template-columns: repeat(2, minmax(0, 1fr));
356
+ gap: 1rem;
357
+ }
358
+
359
+ @media (min-width: 640px) {
360
+ .cols-thumbnail {
361
+ grid-template-columns: repeat(3, minmax(0, 1fr));
362
+ }
363
+ }
364
+
365
+ @media (min-width: 1024px) {
366
+ .cols-thumbnail {
367
+ grid-template-columns: repeat(4, minmax(0, 1fr));
368
+ }
369
+ }
370
+
371
+ @media (min-width: 1280px) {
372
+ .cols-thumbnail {
373
+ grid-template-columns: repeat(5, minmax(0, 1fr));
374
+ }
375
+ }
376
+
377
+ .grid-item {
378
+ display: flex;
379
+ flex-direction: column;
380
+ align-items: center;
381
+ justify-content: center;
382
+ text-align: center;
383
+ margin: 0 auto;
384
+ width: 100%;
385
+ }
386
+
387
+ .grid-figure {
388
+ width: 100%;
389
+ display: flex;
390
+ flex-direction: column;
391
+ align-items: center;
392
+ justify-content: center;
393
+ margin: 0;
394
+ }
395
+
396
+ .grid-link {
397
+ text-decoration: none;
398
+ color: inherit;
399
+ display: block;
400
+ width: 100%;
401
+ }
402
+
403
+ .grid-link:hover {
404
+ opacity: 0.9;
405
+ }
406
+
407
+ .grid-img {
408
+ max-width: 100% !important;
409
+ box-sizing: border-box;
410
+ border-radius: 8px;
411
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
412
+ display: block;
413
+ height: auto;
414
+ margin: 0 auto;
415
+ }
416
+
417
+ .clickable {
418
+ cursor: pointer !important;
419
+ }
420
+
421
+ .grid-img.clickable:hover {
422
+ transform: scale(1.01);
423
+ transition: transform 0.2s ease-in-out;
424
+ }
425
+
426
+ .grid-caption {
427
+ font-size: 0.85rem;
428
+ margin-top: 0.5rem;
429
+ text-align: center;
430
+ color: #64748b;
431
+ font-weight: 500;
432
+ }
433
+
434
+ /* Gallery layout style */
435
+ .gallery-layout-wrapper {
436
+ display: flex;
437
+ flex-direction: column;
438
+ gap: 1rem;
439
+ width: 100%;
440
+ }
441
+
442
+ .featured-wrapper {
443
+ width: 100%;
444
+ overflow: hidden;
445
+ border-radius: 8px;
446
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
447
+ border: 1px solid #f1f5f9;
448
+ }
449
+
450
+ .featured-wrapper:hover {
451
+ opacity: 0.95;
452
+ transition: opacity 0.2s ease-in-out;
453
+ }
454
+
455
+ .featured-img {
456
+ width: 100%;
457
+ height: auto;
458
+ display: block;
459
+ object-fit: cover;
460
+ }
461
+
462
+ .thumbnail-item {
463
+ aspect-ratio: 16 / 9;
464
+ overflow: hidden;
465
+ border-radius: 8px;
466
+ border: 1px solid #f1f5f9;
467
+ background-color: #f8fafc;
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ }
472
+
473
+ .thumbnail-item:hover {
474
+ transform: scale(1.02);
475
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
476
+ transition: all 0.2s ease-in-out;
477
+ }
478
+
479
+ .thumbnail-img {
480
+ width: 100%;
481
+ height: 100%;
482
+ object-fit: cover;
483
+ display: block;
484
+ }
485
+
486
+ /* Lightbox backdrop overlay */
487
+ .lightbox-overlay {
488
+ position: fixed;
489
+ top: 0;
490
+ left: 0;
491
+ width: 100%;
492
+ height: 100%;
493
+ background-color: rgba(0, 0, 0, 0.9);
494
+ z-index: 99999;
495
+ display: flex;
496
+ flex-direction: column;
497
+ align-items: center;
498
+ justify-content: center;
499
+ user-select: none;
500
+ }
501
+
502
+ .lightbox-header {
503
+ position: absolute;
504
+ top: 0;
505
+ left: 0;
506
+ right: 0;
507
+ height: 64px;
508
+ display: flex;
509
+ align-items: center;
510
+ justify-content: space-between;
511
+ padding: 0 1.5rem;
512
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0.6), transparent);
513
+ z-index: 100001;
514
+ color: #ffffff;
515
+ font-family: sans-serif;
516
+ }
517
+
518
+ .lightbox-counter {
519
+ font-size: 0.875rem;
520
+ font-weight: 500;
521
+ letter-spacing: 0.05em;
522
+ }
523
+
524
+ .lightbox-btn {
525
+ background-color: rgba(0, 0, 0, 0.3);
526
+ border: none;
527
+ color: rgba(255, 255, 255, 0.8);
528
+ cursor: pointer !important;
529
+ display: flex;
530
+ align-items: center;
531
+ justify-content: center;
532
+ border-radius: 50%;
533
+ transition: all 0.2s ease-in-out;
534
+ }
535
+
536
+ .lightbox-btn:hover {
537
+ background-color: rgba(255, 255, 255, 0.2);
538
+ color: #ffffff;
539
+ }
540
+
541
+ .lightbox-close {
542
+ width: 40px;
543
+ height: 40px;
544
+ padding: 0;
545
+ }
546
+
547
+ .lightbox-icon {
548
+ width: 24px;
549
+ height: 24px;
550
+ }
551
+
552
+ .lightbox-nav {
553
+ position: absolute;
554
+ top: 50%;
555
+ transform: translateY(-50%);
556
+ width: 48px !important;
557
+ height: 48px !important;
558
+ min-width: 48px !important;
559
+ max-width: 48px !important;
560
+ min-height: 48px !important;
561
+ max-height: 48px !important;
562
+ flex-shrink: 0 !important;
563
+ box-sizing: border-box !important;
564
+ border-radius: 50% !important;
565
+ z-index: 100002;
566
+ }
567
+
568
+ .lightbox-prev {
569
+ left: 1.5rem;
570
+ }
571
+
572
+ .lightbox-next {
573
+ right: 1.5rem;
574
+ }
575
+
576
+ .lightbox-nav-icon {
577
+ width: 28px;
578
+ height: 28px;
579
+ }
580
+
581
+ .lightbox-content {
582
+ max-width: 90%;
583
+ max-height: 80%;
584
+ display: flex;
585
+ flex-direction: column;
586
+ align-items: center;
587
+ justify-content: center;
588
+ z-index: 100000;
589
+ }
590
+
591
+ .lightbox-image {
592
+ max-width: 100%;
593
+ max-height: 75vh;
594
+ object-fit: contain;
595
+ border-radius: 4px;
596
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
597
+ }
598
+
599
+ .lightbox-caption {
600
+ color: rgba(255, 255, 255, 0.9);
601
+ font-size: 0.875rem;
602
+ margin-top: 1rem;
603
+ text-align: center;
604
+ max-width: 36rem;
605
+ padding: 0 1rem;
606
+ }
607
+
608
+ /* Transition Animations */
609
+ .fade-enter-active,
610
+ .fade-leave-active {
611
+ transition: opacity 0.25s ease;
612
+ }
613
+
614
+ .fade-enter-from,
615
+ .fade-leave-to {
616
+ opacity: 0;
617
+ }
618
+
619
+ .mt-4 {
620
+ margin-top: 1rem;
621
+ }
622
+ </style>
@@ -0,0 +1,27 @@
1
+ import SingleTweet from "./SingleTweet.vue";
2
+
3
+ describe("<SingleTweet />", () => {
4
+ it("renders consent fallback if cookies are not allowed", () => {
5
+ cy.mount(SingleTweet, {
6
+ props: {
7
+ id: "12345",
8
+ mockConsent: false,
9
+ },
10
+ });
11
+
12
+ cy.contains("Cookie Consent Required").should("be.visible");
13
+ cy.get("blockquote").should("not.exist");
14
+ });
15
+
16
+ it("renders tweet blockquote if cookies are allowed", () => {
17
+ cy.mount(SingleTweet, {
18
+ props: {
19
+ id: "12345",
20
+ mockConsent: true,
21
+ },
22
+ });
23
+
24
+ cy.get("blockquote.twitter-tweet").should("exist");
25
+ cy.contains("Cookie Consent Required").should("not.exist");
26
+ });
27
+ });