@eox/pages-theme-eox 1.8.0 → 2.0.0-alpha.1

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +83 -0
  3. package/cypress/support/mocks/vitepress.js +8 -1
  4. package/cypress.config.js +5 -0
  5. package/package.json +1 -1
  6. package/src/Layout.vue +58 -1
  7. package/src/authors.data.js +14 -0
  8. package/src/components/BlogList.vue +636 -0
  9. package/src/components/BlogPost.vue +450 -0
  10. package/src/components/FeatureCard/Index.vue +370 -26
  11. package/src/components/HeroSection.vue +1 -1
  12. package/src/components/ImageCarousel.cy.js +22 -0
  13. package/src/components/ImageCarousel.vue +268 -0
  14. package/src/components/ImageElement.cy.js +46 -0
  15. package/src/components/ImageElement.vue +269 -0
  16. package/src/components/ImageGallery.cy.js +52 -0
  17. package/src/components/ImageGallery.vue +622 -0
  18. package/src/components/SectionHeader.cy.js +54 -0
  19. package/src/components/SectionHeader.vue +115 -0
  20. package/src/components/SingleTweet.cy.js +27 -0
  21. package/src/components/SingleTweet.vue +155 -0
  22. package/src/components/TagList.vue +293 -0
  23. package/src/components/TeamPage.vue +333 -0
  24. package/src/components/TestimonialsSection.cy.js +79 -0
  25. package/src/components/TestimonialsSection.vue +265 -0
  26. package/src/index.js +21 -0
  27. package/src/pages/author/[author].md +4 -0
  28. package/src/pages/author/[author].paths.js +60 -0
  29. package/src/pages/blog.md +4 -0
  30. package/src/pages/tag/[tag].md +4 -0
  31. package/src/pages/tag/[tag].paths.js +49 -0
  32. package/src/pages/tag.md +4 -0
  33. package/src/pages/team.md +4 -0
  34. package/src/posts.data.js +96 -0
  35. package/src/style.css +160 -0
  36. package/src/tagUtils.js +148 -0
  37. package/src/team.data.js +51 -0
  38. package/src/vitepressConfig.mjs +97 -0
@@ -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,54 @@
1
+ import SectionHeader from "./SectionHeader.vue";
2
+
3
+ describe("<SectionHeader />", () => {
4
+ it("renders standard props correctly", () => {
5
+ const tag = "Viewing Ready & Basic";
6
+ const icon = "mdi-star-check-outline";
7
+ const title = "What our users say about Viewing Products";
8
+ const subtitle =
9
+ "Optional paragraph providing context and detail for this section.";
10
+
11
+ cy.mount(SectionHeader, {
12
+ props: {
13
+ tag,
14
+ icon,
15
+ title,
16
+ subtitle,
17
+ },
18
+ });
19
+
20
+ // Check that tag badge exists and contains correct icon and text classes
21
+ cy.get(".section-badge-tag").should("exist");
22
+ cy.get(".section-badge-icon").should("have.class", icon);
23
+ cy.get(".section-badge-text").should("contain", tag);
24
+
25
+ // Check title
26
+ cy.get(".section-display-title").should("contain", title);
27
+
28
+ // Check subtitle
29
+ cy.get(".section-subtitle").should("contain", subtitle);
30
+ });
31
+
32
+ it("renders with title slot and default slot", () => {
33
+ cy.mount(SectionHeader, {
34
+ slots: {
35
+ title: "Slot-based Title",
36
+ default: "Slot-based Subtitle",
37
+ },
38
+ });
39
+
40
+ cy.get(".section-display-title").should("contain", "Slot-based Title");
41
+ cy.get(".section-subtitle").should("contain", "Slot-based Subtitle");
42
+ });
43
+
44
+ it("hides badge if no tag or icon is provided", () => {
45
+ cy.mount(SectionHeader, {
46
+ props: {
47
+ title: "Just Title",
48
+ },
49
+ });
50
+
51
+ cy.get(".section-badge-tag").should("not.exist");
52
+ cy.get(".section-subtitle").should("not.exist");
53
+ });
54
+ });