@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
@@ -1,5 +1,9 @@
1
1
  <script setup>
2
- const { title, content, link, secondaryLink, icon } = defineProps({
2
+ import { computed } from "vue";
3
+ import { data as authorsList } from "../../authors.data.js";
4
+ import { getTagColor } from "../../tagUtils.js";
5
+
6
+ const props = defineProps({
3
7
  title: String,
4
8
  content: String,
5
9
  image: {
@@ -7,7 +11,7 @@ const { title, content, link, secondaryLink, icon } = defineProps({
7
11
  required: false,
8
12
  },
9
13
  chips: {
10
- /** @type {import('vue').PropType<{ text: string; class?: string }[]>} */
14
+ /** @type {import('vue').PropType<{ text: string; link?: string; href?: string; class?: string }[]>} */
11
15
  type: Array,
12
16
  required: false,
13
17
  default: () => [],
@@ -33,18 +37,104 @@ const { title, content, link, secondaryLink, icon } = defineProps({
33
37
  type: Object,
34
38
  required: false,
35
39
  },
40
+ date: {
41
+ type: String,
42
+ required: false,
43
+ },
44
+ authors: {
45
+ type: Array,
46
+ required: false,
47
+ default: () => [],
48
+ },
49
+ variant: {
50
+ type: String,
51
+ required: false,
52
+ default: "default",
53
+ },
36
54
  });
55
+
37
56
  const iconStyle = {
38
- width: (icon?.width ?? 40) + "px",
39
- height: (icon?.height ?? 40) + "px",
57
+ width: (props.icon?.width ?? 40) + "px",
58
+ height: (props.icon?.height ?? 40) + "px",
40
59
  };
60
+
61
+ // Check if we should render a full-width cover image
62
+ const isCoverLayout = computed(() => {
63
+ return props.image && (props.variant === "cover" || props.date || props.authors.length);
64
+ });
65
+
66
+ // Check if the card itself is fully clickable (has a link but NO link text)
67
+ const isClickableCard = computed(() => {
68
+ return props.link && !props.link.text;
69
+ });
70
+
71
+ function getAuthorName(authorId) {
72
+ if (!authorId) return "EOX Experts";
73
+ const author = authorsList.find(
74
+ (a) => a.id.toLowerCase().trim() === authorId.toLowerCase().trim()
75
+ );
76
+ return author ? author.name : authorId;
77
+ }
78
+
79
+ function getTagStyle(tag) {
80
+ const color = getTagColor(tag);
81
+ if (color.startsWith("rgb(255, 94, 94)")) {
82
+ return {
83
+ backgroundColor: "rgba(255, 94, 94, 0.15)",
84
+ border: "1px solid rgb(255, 94, 94)",
85
+ color: "#d32f2f"
86
+ };
87
+ } else if (color.startsWith("rgb(0, 177, 176)")) {
88
+ return {
89
+ backgroundColor: "rgba(0, 177, 176, 0.15)",
90
+ border: "1px solid rgb(0, 177, 176)",
91
+ color: "#00796b"
92
+ };
93
+ } else if (color.startsWith("rgb(219, 173, 66)")) {
94
+ return {
95
+ backgroundColor: "rgba(219, 173, 66, 0.15)",
96
+ border: "1px solid rgb(219, 173, 66)",
97
+ color: "#f57c00"
98
+ };
99
+ } else if (color.startsWith("rgb(218, 71, 228)")) {
100
+ return {
101
+ backgroundColor: "rgba(218, 71, 228, 0.15)",
102
+ border: "1px solid rgb(218, 71, 228)",
103
+ color: "#c2185b"
104
+ };
105
+ }
106
+ return {
107
+ backgroundColor: "rgba(0, 0, 0, 0.05)",
108
+ border: "1px solid rgba(0, 0, 0, 0.1)",
109
+ color: "#475569"
110
+ };
111
+ }
112
+
113
+ function formatDate(dateString) {
114
+ if (!dateString) return "";
115
+ const date = new Date(dateString);
116
+ return date.toLocaleDateString("en-US", {
117
+ year: "numeric",
118
+ month: "short",
119
+ day: "numeric",
120
+ });
121
+ }
41
122
  </script>
123
+
42
124
  <template>
43
125
  <article
44
- class="vertical large-padding no-margin"
45
- :class="link && !link.text ? 'small-elevate' : 'no-elevate'"
126
+ class="vertical large-padding no-margin theme-feature-card"
127
+ :class="[
128
+ isClickableCard ? 'small-elevate clickable-card' : 'no-elevate',
129
+ isCoverLayout ? 'has-cover-image' : ''
130
+ ]"
46
131
  >
47
- <div>
132
+ <!-- Cover Image for cover/blog/rich cards -->
133
+ <a v-if="isCoverLayout" :href="link?.href" class="card-cover-link">
134
+ <img :src="image" alt="" class="card-cover-image" loading="lazy" />
135
+ </a>
136
+
137
+ <div class="card-content-wrapper">
48
138
  <i
49
139
  v-if="typeof icon === 'string' && icon.startsWith('mdi-')"
50
140
  :class="`mdi ${icon}`"
@@ -55,23 +145,73 @@ const iconStyle = {
55
145
  v-html="icon.html"
56
146
  class="icon"
57
147
  ></div>
58
- <img v-else-if="image" :src="image" alt="" class="icon" />
59
- <h5 class="small">{{ title }}</h5>
148
+ <!-- Render small icon image only if NOT rendering a full cover image -->
149
+ <img v-else-if="image && !isCoverLayout" :src="image" alt="" class="icon" />
150
+
151
+ <!-- Metadata & Tags directly under place/author -->
152
+ <div v-if="date || (authors && authors.length)" class="metadata-wrapper">
153
+ <div class="blog-metadata">
154
+ <span v-if="date" class="date-span">
155
+ <i class="mdi mdi-clock-outline"></i>
156
+ <time :datetime="date">{{ formatDate(date) }}</time>
157
+ </span>
158
+ <span v-if="authors && authors.length" class="author-span">
159
+ <span class="author-icon">
160
+ <i class="mdi mdi-account-outline"></i>
161
+ </span>
162
+ <span v-for="(authorId, idx) in authors" :key="authorId" class="author-item">
163
+ <a :href="`/author/${authorId}/`" class="author-link">{{ getAuthorName(authorId) }}</a><span v-if="idx < authors.length - 1" class="author-separator">,</span>
164
+ </span>
165
+ </span>
166
+ </div>
167
+
168
+ <!-- Tags directly under metadata if chips are provided and this is a rich card -->
169
+ <div v-if="chips && chips.length" class="blog-tags-container">
170
+ <a
171
+ v-for="chip in chips"
172
+ :key="chip.text"
173
+ :href="chip.link || chip.href || `/tag/${chip.text}/`"
174
+ :style="getTagStyle(chip.text)"
175
+ class="tag-link"
176
+ >
177
+ {{ chip.text }}
178
+ </a>
179
+ </div>
180
+ </div>
181
+
182
+ <h5 class="small card-title-text">
183
+ <a v-if="link?.href" :href="link.href">{{ title }}</a>
184
+ <span v-else>{{ title }}</span>
185
+ </h5>
186
+
187
+ <!-- Standard chips (rendered only for normal cards) -->
60
188
  <div
61
- v-if="chips && chips.length"
189
+ v-if="chips && chips.length && !date && !authors.length"
62
190
  class="row wrap"
63
191
  style="gap: 0.5rem; margin-bottom: 0.5rem"
64
192
  >
65
- <span
66
- v-for="chip in chips"
67
- :key="chip.text"
68
- class="chip small-chip border"
69
- :class="chip.class"
70
- >
71
- {{ chip.text }}
72
- </span>
193
+ <template v-for="chip in chips" :key="chip.text">
194
+ <a
195
+ v-if="chip.link || chip.href"
196
+ :href="chip.link || chip.href"
197
+ :style="getTagStyle(chip.text)"
198
+ class="chip small-chip border clickable-chip"
199
+ :class="chip.class"
200
+ >
201
+ {{ chip.text }}
202
+ </a>
203
+ <span
204
+ v-else
205
+ :style="getTagStyle(chip.text)"
206
+ class="chip small-chip border clickable-chip"
207
+ :class="chip.class"
208
+ >
209
+ {{ chip.text }}
210
+ </span>
211
+ </template>
73
212
  </div>
74
- <p v-if="metadata && metadata.length" class="small-text">
213
+
214
+ <p v-if="metadata && metadata.length && !date && !authors.length" class="small-text">
75
215
  <span
76
216
  v-for="(item, index) in metadata"
77
217
  :key="index"
@@ -87,14 +227,15 @@ const iconStyle = {
87
227
  </span>
88
228
  </p>
89
229
 
90
- <p v-if="$slots.default">
230
+ <p v-if="$slots.default" class="card-body-text">
91
231
  <slot>{{ content }}</slot>
92
232
  </p>
93
- <div v-else v-html="content"></div>
233
+ <div v-else-if="content" v-html="content" class="card-body-text"></div>
94
234
  </div>
235
+
95
236
  <nav
96
237
  v-if="link || secondaryLink"
97
- class="row wrap align-center"
238
+ class="row wrap align-center card-navigation"
98
239
  style="gap: 1.5rem; margin-right: auto; position: relative; z-index: 11"
99
240
  >
100
241
  <a
@@ -121,6 +262,7 @@ const iconStyle = {
121
262
  </nav>
122
263
  </article>
123
264
  </template>
265
+
124
266
  <style scoped>
125
267
  /**
126
268
  * Undo Vitepress missing with @eox/ui styles
@@ -142,17 +284,33 @@ const iconStyle = {
142
284
  /**
143
285
  * Custom styles
144
286
  */
145
- article {
287
+ .theme-feature-card {
146
288
  justify-content: space-between;
147
- transition: all 0.3s ease-in-out;
148
289
  width: 100%;
290
+ border-radius: 16px !important;
291
+ overflow: hidden !important;
292
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important;
293
+ }
294
+
295
+ /* Clickable Cards Hover System */
296
+ .theme-feature-card.clickable-card {
297
+ cursor: pointer;
298
+ transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1), box-shadow 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
299
+ will-change: transform, box-shadow;
149
300
  }
301
+
302
+ .theme-feature-card.clickable-card:hover {
303
+ transform: translateY(-4px) !important;
304
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12), 0 4px 8px rgba(0, 0, 0, 0.04) !important;
305
+ }
306
+
150
307
  .small-chip {
151
308
  font-size: 0.75rem;
152
- padding: 0.125rem 0.375rem;
309
+ padding: 4px 10px !important;
153
310
  height: auto;
154
- line-height: normal;
311
+ line-height: 1.2 !important;
155
312
  margin: 0;
313
+ border-radius: 6px !important;
156
314
  }
157
315
  .arrow {
158
316
  transition: transform 0.2s;
@@ -163,4 +321,190 @@ article {
163
321
  .button.transparent:hover:after {
164
322
  background: none;
165
323
  }
324
+
325
+ /* Card with Cover Image Layout */
326
+ .theme-feature-card.has-cover-image {
327
+ padding: 0 !important;
328
+ display: flex;
329
+ flex-direction: column;
330
+ }
331
+
332
+ .theme-feature-card.has-cover-image .card-cover-link {
333
+ display: block;
334
+ width: 100%;
335
+ aspect-ratio: 16 / 9;
336
+ overflow: hidden;
337
+ position: relative;
338
+ flex-shrink: 0;
339
+ border-top-left-radius: 16px !important;
340
+ border-top-right-radius: 16px !important;
341
+ border-bottom-left-radius: 0 !important;
342
+ border-bottom-right-radius: 0 !important;
343
+ }
344
+
345
+ .theme-feature-card.has-cover-image .card-cover-image {
346
+ width: 100%;
347
+ height: 100%;
348
+ object-fit: cover;
349
+ transition: transform 0.3s ease;
350
+ border-top-left-radius: 16px !important;
351
+ border-top-right-radius: 16px !important;
352
+ border-bottom-left-radius: 0 !important;
353
+ border-bottom-right-radius: 0 !important;
354
+ }
355
+
356
+ /* Zoom Image on Hover ONLY if the card itself is clickable */
357
+ .theme-feature-card.has-cover-image.clickable-card .card-cover-image {
358
+ transition: transform 0.5s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
359
+ }
360
+
361
+ .theme-feature-card.has-cover-image.clickable-card:hover .card-cover-image {
362
+ transform: scale(1.04) !important;
363
+ }
364
+
365
+ .theme-feature-card.has-cover-image .card-content-wrapper {
366
+ padding: 1.5rem;
367
+ display: flex;
368
+ flex-direction: column;
369
+ flex-grow: 1;
370
+ }
371
+
372
+ .theme-feature-card.has-cover-image .card-navigation {
373
+ padding: 0 1.5rem 1.5rem 1.5rem;
374
+ margin-top: auto;
375
+ }
376
+
377
+ /* Metadata Styling */
378
+ .metadata-wrapper {
379
+ margin-bottom: 16px;
380
+ text-align: left;
381
+ }
382
+
383
+ .blog-metadata {
384
+ display: block !important;
385
+ font-size: 12px !important;
386
+ color: #64748b !important;
387
+ line-height: 1.6 !important;
388
+ margin-bottom: 8px;
389
+ }
390
+
391
+ .date-span {
392
+ display: inline-flex;
393
+ align-items: center;
394
+ gap: 4px;
395
+ white-space: nowrap !important;
396
+ margin-right: 12px;
397
+ vertical-align: middle;
398
+ }
399
+
400
+ .date-span i {
401
+ font-size: 13px !important;
402
+ color: #64748b !important;
403
+ vertical-align: middle;
404
+ }
405
+
406
+ .author-span {
407
+ display: inline;
408
+ }
409
+
410
+ .author-icon {
411
+ display: inline-flex;
412
+ align-items: center;
413
+ margin-right: 4px;
414
+ vertical-align: middle;
415
+ }
416
+
417
+ .author-icon i {
418
+ font-size: 13px !important;
419
+ color: #64748b !important;
420
+ vertical-align: middle;
421
+ }
422
+
423
+ .author-item {
424
+ display: inline;
425
+ white-space: nowrap !important;
426
+ }
427
+
428
+ .author-link {
429
+ color: #64748b !important;
430
+ text-decoration: none !important;
431
+ font-size: 12px !important;
432
+ font-weight: 600;
433
+ transition: color 0.15s ease;
434
+ vertical-align: middle;
435
+ }
436
+
437
+ .author-link:hover {
438
+ color: var(--vp-c-brand) !important;
439
+ text-decoration: underline !important;
440
+ }
441
+
442
+ .author-separator {
443
+ color: #64748b !important;
444
+ font-size: 12px !important;
445
+ vertical-align: middle;
446
+ margin-right: 4px;
447
+ }
448
+
449
+ .date-span time {
450
+ font-size: 12px !important;
451
+ }
452
+
453
+ /* Tags Styling */
454
+ .blog-tags-container {
455
+ display: flex;
456
+ flex-wrap: wrap;
457
+ gap: 6px;
458
+ margin-top: 8px;
459
+ }
460
+
461
+ .tag-link {
462
+ display: inline-block !important;
463
+ font-size: 12px !important;
464
+ font-weight: 700 !important;
465
+ padding: 4px 10px !important;
466
+ border-radius: 6px !important;
467
+ text-decoration: none !important;
468
+ line-height: 1.2 !important;
469
+ transition: opacity 0.2s ease;
470
+ }
471
+
472
+ .tag-link:hover {
473
+ opacity: 0.8 !important;
474
+ }
475
+
476
+ /* card title overrides to make sure it respects the 20px request */
477
+ .VPHome .vp-doc h5.small.card-title-text {
478
+ font-size: 20px !important;
479
+ line-height: 1.4 !important;
480
+ font-weight: 700 !important;
481
+ margin-bottom: 0.75rem !important;
482
+ }
483
+
484
+ .card-title-text a {
485
+ color: #0f172a !important;
486
+ text-decoration: none !important;
487
+ }
488
+
489
+ .card-title-text a:hover {
490
+ color: var(--vp-c-brand) !important;
491
+ }
492
+
493
+ .card-body-text {
494
+ font-size: 0.875rem !important;
495
+ line-height: 1.6 !important;
496
+ color: #475569 !important;
497
+ }
498
+
499
+ /* Clickable standard chips styling */
500
+ .clickable-chip {
501
+ text-decoration: none !important;
502
+ color: var(--vp-c-text-2) !important;
503
+ transition: all 0.2s ease;
504
+ border-radius: 6px !important;
505
+ border: 1px solid transparent !important;
506
+ }
507
+ .clickable-chip:hover {
508
+ opacity: 0.8 !important;
509
+ }
166
510
  </style>
@@ -75,7 +75,7 @@ import { trackEvent } from "../helpers";
75
75
  const { frontmatter } = useData();
76
76
  </script>
77
77
 
78
- <style>
78
+ <style scoped>
79
79
  header {
80
80
  margin-top: calc(var(--vp-nav-height) * -1);
81
81
  padding-top: 10rem !important;
@@ -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
+ });