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

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 (47) hide show
  1. package/.gitlab-ci.yml +4 -1
  2. package/README.md +75 -11
  3. package/cypress/support/mocks/authors.data.js +14 -0
  4. package/cypress/support/mocks/helpers.js +65 -4
  5. package/cypress.config.js +9 -0
  6. package/package.json +3 -1
  7. package/src/Layout.vue +14 -3
  8. package/src/components/BlogList.vue +74 -55
  9. package/src/components/BlogPost.vue +46 -24
  10. package/src/components/CTASection.vue +18 -4
  11. package/src/components/FeatureCard/Index.vue +113 -26
  12. package/src/components/FeatureCard/Poster.vue +25 -3
  13. package/src/components/FeatureSection.cy.js +16 -0
  14. package/src/components/FeatureSection.vue +3 -8
  15. package/src/components/FeaturesGallery.vue +34 -2
  16. package/src/components/Footer.cy.js +52 -2
  17. package/src/components/Footer.vue +17 -5
  18. package/src/components/HeroSection.cy.js +20 -0
  19. package/src/components/HeroSection.vue +139 -24
  20. package/src/components/ImageCarousel.vue +11 -2
  21. package/src/components/ImageElement.vue +16 -4
  22. package/src/components/ImageGallery.vue +29 -4
  23. package/src/components/LogoSection.vue +26 -3
  24. package/src/components/MobileNavDropdown.vue +13 -3
  25. package/src/components/NavBar.cy.js +15 -0
  26. package/src/components/NavBar.vue +149 -21
  27. package/src/components/NavDropdown.vue +12 -2
  28. package/src/components/OpenPositions.vue +292 -0
  29. package/src/components/TagList.vue +32 -14
  30. package/src/components/TeamPage.vue +34 -20
  31. package/src/components/TestimonialsSection.cy.js +2 -5
  32. package/src/components/TestimonialsSection.vue +119 -16
  33. package/src/components/TextSection.cy.js +115 -0
  34. package/src/components/TextSection.vue +396 -0
  35. package/src/helpers.js +182 -37
  36. package/src/helpers.test.js +181 -0
  37. package/src/index.js +4 -0
  38. package/src/pages/author/[author].md +1 -0
  39. package/src/pages/author/[author].paths.js +2 -2
  40. package/src/pages/blog.md +1 -0
  41. package/src/pages/tag/[tag].md +1 -0
  42. package/src/pages/tag.md +1 -0
  43. package/src/pages/team.md +2 -1
  44. package/src/posts.data.js +39 -1
  45. package/src/style.css +232 -11
  46. package/src/tagUtils.js +5 -5
  47. package/src/vitepressConfig.mjs +6 -3
@@ -2,7 +2,8 @@
2
2
  <section class="logo-section">
3
3
  <div ref="logo-row" class="logo-row">
4
4
  <component
5
- v-for="logo in logos"
5
+ v-for="logo in normalizedLogos"
6
+ :key="logo.alt"
6
7
  :is="logo.link ? 'a' : 'div'"
7
8
  :href="logo.link || undefined"
8
9
  :target="logo.target || undefined"
@@ -15,15 +16,36 @@
15
16
  </template>
16
17
 
17
18
  <script setup>
18
- import { useTemplateRef, onMounted } from "vue";
19
+ import { useTemplateRef, onMounted, computed } from "vue";
20
+ import { withBase } from "vitepress";
19
21
 
20
22
  const props = defineProps(["baseHeight", "logos", "strength"]);
21
23
  const logoRow = useTemplateRef("logo-row");
22
24
 
25
+ function resolveUrl(url) {
26
+ if (!url) return "";
27
+ if (url.startsWith("http") || url.startsWith("//")) return url;
28
+ const base = withBase("/");
29
+ if (base && base !== "/" && url.startsWith(base)) return url;
30
+ return url.startsWith("/") ? withBase(url) : withBase("/" + url);
31
+ }
32
+
33
+ const normalizedLogos = computed(() => {
34
+ if (!props.logos) return [];
35
+ return props.logos.map((logo) => ({
36
+ ...logo,
37
+ image: resolveUrl(logo.image),
38
+ link: logo.link ? resolveUrl(logo.link) : undefined,
39
+ }));
40
+ });
41
+
23
42
  onMounted(() => {
24
43
  // Function to update CSS variables and display values
25
44
  function updateVariables() {
26
- const baseHeight = props.baseHeight || 4;
45
+ let baseHeight = props.baseHeight || 4;
46
+ if (typeof baseHeight === "string" && baseHeight.endsWith("rem")) {
47
+ baseHeight = baseHeight.slice(0, -3).trim();
48
+ }
27
49
  const strength = props.strength || 1;
28
50
 
29
51
  // Update CSS variables on all .logo-row elements
@@ -46,6 +68,7 @@ onMounted(() => {
46
68
 
47
69
  <style>
48
70
  section.logo-section {
71
+ background-color: var(--surface-container-lowest, #ffffff);
49
72
  margin-block-start: 3rem;
50
73
  }
51
74
  .logo-section .logo-row {
@@ -1,8 +1,11 @@
1
1
  <template>
2
2
  <template v-for="item in items">
3
- <li v-if="item.items && item.text">
3
+ <li
4
+ v-if="item.items && item.text"
5
+ :class="{ active: isActive(item, route.path) }"
6
+ >
4
7
  <details>
5
- <summary>
8
+ <summary :class="{ active: isActive(item, route.path) }">
6
9
  <span class="max" :class="{ active: isActive(item, route.path) }">{{
7
10
  t(item.text, theme.i18n)
8
11
  }}</span>
@@ -14,7 +17,7 @@
14
17
  </details>
15
18
  </li>
16
19
  <MobileNavDropdown v-else-if="item.items" :items="item.items" />
17
- <li v-else>
20
+ <li v-else :class="{ active: isActive(item, route.path) }">
18
21
  <a
19
22
  v-if="item.link"
20
23
  :href="withBase(item.link)"
@@ -46,3 +49,10 @@ defineProps({
46
49
  const route = useRoute();
47
50
  const { theme } = useData();
48
51
  </script>
52
+
53
+ <style scoped>
54
+ a,
55
+ a:hover {
56
+ text-decoration: none !important;
57
+ }
58
+ </style>
@@ -28,6 +28,7 @@ describe("<NavBar />", () => {
28
28
  cy.viewport(1920, 1080);
29
29
  cy.mount(NavBar);
30
30
  cy.get(".nav-desktop img.logo").should("have.attr", "src", "/logo.png");
31
+ cy.get(".nav-desktop img.logo").should("have.css", "height", "28px");
31
32
  });
32
33
 
33
34
  it("renders desktop navigation items", () => {
@@ -140,4 +141,18 @@ describe("<NavBar />", () => {
140
141
  .contains("English")
141
142
  .should("not.exist");
142
143
  });
144
+
145
+ it("handles responsive breakpoints between mobile and desktop navigation", () => {
146
+ // Medium tablet viewport (e.g. iPad portrait 768px): triggers mobile hamburger navigation
147
+ cy.viewport(768, 1024);
148
+ cy.mount(NavBar);
149
+ cy.get(".nav-mobile").should("be.visible");
150
+ cy.get(".nav-desktop").should("not.be.visible");
151
+
152
+ // Medium tablet landscape / small laptop viewport (1024px): triggers desktop navigation
153
+ cy.viewport(1024, 768);
154
+ cy.mount(NavBar);
155
+ cy.get(".nav-desktop").should("be.visible");
156
+ cy.get(".nav-mobile").should("not.be.visible");
157
+ });
143
158
  });
@@ -1,7 +1,7 @@
1
1
  <template>
2
- <div class="top-nav at-top row center-align">
2
+ <div class="top-nav at-top row center-align" :class="{ 'has-hero': hasHero }">
3
3
  <nav class="large-padding no-margin center-align nav-mobile">
4
- <a href="/">
4
+ <a :href="withBase('/')">
5
5
  <img
6
6
  :src="withBase(theme.logo.light)"
7
7
  :alt="`${site.title} logo`"
@@ -20,7 +20,7 @@
20
20
  </button>
21
21
  <dialog id="mobile-menu" class="max">
22
22
  <nav>
23
- <a data-ui="#mobile-menu" href="/">
23
+ <a data-ui="#mobile-menu" :href="withBase('/')">
24
24
  <img
25
25
  :src="withBase(theme.logo.light)"
26
26
  :alt="`${site.title} logo`"
@@ -33,10 +33,17 @@
33
33
  </button>
34
34
  </nav>
35
35
  <ul class="list border large-space">
36
- <li v-for="item in theme.nav.filter((item) => !item.action)">
36
+ <li
37
+ v-for="item in theme.nav.filter((item) => !item.action)"
38
+ :class="{ active: isActive(item, route.path) }"
39
+ >
37
40
  <details v-if="item.items">
38
- <summary>
39
- <span class="max">{{ t(item.text, theme.i18n) }}</span>
41
+ <summary :class="{ active: isActive(item, route.path) }">
42
+ <span
43
+ class="max"
44
+ :class="{ active: isActive(item, route.path) }"
45
+ >{{ t(item.text, theme.i18n) }}</span
46
+ >
40
47
  <i class="mdi mdi-chevron-down"></i>
41
48
  </summary>
42
49
  <ul class="list">
@@ -49,7 +56,10 @@
49
56
  :href="withBase(item.link)"
50
57
  :target="item.target"
51
58
  :rel="item.rel"
52
- :class="item.action ? 'button large medium-elevate cta' : ''"
59
+ :class="[
60
+ item.action ? 'button large medium-elevate cta' : '',
61
+ { active: isActive(item, route.path) },
62
+ ]"
53
63
  >
54
64
  <span>{{ t(item.text, theme.i18n) }}</span>
55
65
  </a>
@@ -119,7 +129,7 @@
119
129
  </dialog>
120
130
  </nav>
121
131
  <nav class="large-padding no-margin center-align row holder nav-desktop">
122
- <a href="/">
132
+ <a :href="withBase('/')">
123
133
  <img
124
134
  :src="withBase(theme.logo.light)"
125
135
  :alt="`${site.title} logo`"
@@ -135,7 +145,10 @@
135
145
  <div class="space"></div>
136
146
  <nav>
137
147
  <ul class="left-align no-margin">
138
- <li v-for="item in theme.nav.filter((i) => !i.action)">
148
+ <li
149
+ v-for="item in theme.nav.filter((i) => !i.action)"
150
+ :class="{ active: isActive(item, route.path) }"
151
+ >
139
152
  <div v-if="item.items">
140
153
  <button
141
154
  class="button text no-round"
@@ -229,7 +242,7 @@ import { t, trackEvent, isActive } from "../helpers";
229
242
  import NavDropdown from "./NavDropdown.vue";
230
243
  import MobileNavDropdown from "./MobileNavDropdown.vue";
231
244
 
232
- const { site, theme, localeIndex } = useData();
245
+ const { site, theme, localeIndex, frontmatter, page } = useData();
233
246
  const route = useRoute();
234
247
 
235
248
  const langs = computed(() => {
@@ -239,7 +252,7 @@ const langs = computed(() => {
239
252
  }
240
253
  return Object.entries(locales).map(([id, locale]) => {
241
254
  const currentPath = route.path;
242
- const base = withBase("");
255
+ const base = withBase("/");
243
256
  let pathRelativeToBase = currentPath;
244
257
  if (base !== "/" && currentPath.startsWith(base)) {
245
258
  pathRelativeToBase = currentPath.substring(base.length);
@@ -271,6 +284,14 @@ const langs = computed(() => {
271
284
  });
272
285
  });
273
286
 
287
+ const hasHero = computed(() => {
288
+ return Boolean(
289
+ frontmatter.value.hero ||
290
+ frontmatter.value.layout === "home" ||
291
+ page.value.relativePath === "index.md",
292
+ );
293
+ });
294
+
274
295
  if (!import.meta.env.SSR) {
275
296
  const scrollListener = () => {
276
297
  const nav = document.querySelector(".top-nav");
@@ -301,7 +322,24 @@ if (!import.meta.env.SSR) {
301
322
  .top-nav.at-top {
302
323
  box-shadow: none;
303
324
  }
304
- .Layout.layout-home .top-nav.at-top .nav-desktop .button.primary,
325
+ .top-nav.at-top .nav-desktop .button.primary,
326
+ .top-nav.has-hero.at-top .nav-desktop .button.primary,
327
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.primary {
328
+ background: #ffffff !important;
329
+ color: #000c14 !important;
330
+ border-radius: 9999px;
331
+ font-weight: 500;
332
+ display: inline-flex;
333
+ }
334
+ .top-nav.at-top .nav-desktop .button.primary i.mdi-arrow-right,
335
+ .top-nav.has-hero.at-top .nav-desktop .button.primary i.mdi-arrow-right,
336
+ .Layout.layout-home
337
+ .top-nav.at-top
338
+ .nav-desktop
339
+ .button.primary
340
+ i.mdi-arrow-right {
341
+ display: none;
342
+ }
305
343
  .Layout.layout-home .top-nav.at-top .nav-desktop .button.secondary,
306
344
  .Layout.layout-home .top-nav.at-top .nav-desktop .button.alt {
307
345
  display: none;
@@ -317,47 +355,114 @@ if (!import.meta.env.SSR) {
317
355
  padding-top: 0.5rem !important;
318
356
  padding-bottom: 0.5rem !important;
319
357
  }
320
- .Layout.layout-home .top-nav.at-top {
358
+ .Layout.layout-home .top-nav.at-top,
359
+ .top-nav.has-hero.at-top {
321
360
  background: transparent;
322
361
  color: #f7f8f8;
323
362
  }
324
363
  .Layout .top-nav nav .button {
325
364
  color: var(--on-surface);
326
365
  }
327
- .Layout.layout-home .top-nav.at-top .nav-desktop .button {
366
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button,
367
+ .top-nav.has-hero.at-top .nav-desktop .button {
328
368
  color: var(--on-surface);
329
369
  background: var(--surface);
330
370
  }
331
- .Layout.layout-home .top-nav.at-top .nav-desktop .button.text {
371
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.text,
372
+ .top-nav.has-hero.at-top .nav-desktop .button.text {
332
373
  color: #f7f8f8;
333
374
  background: none;
334
375
  }
335
- .Layout.layout-home .top-nav.at-top .nav-desktop .button.border {
376
+ .Layout.layout-home
377
+ .top-nav.at-top
378
+ .nav-desktop
379
+ .button.text:hover:not(.active),
380
+ .top-nav.has-hero.at-top .nav-desktop .button.text:hover:not(.active) {
381
+ background: rgba(255, 255, 255, 0.1);
382
+ color: #ffffff;
383
+ }
384
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.text.active,
385
+ .top-nav.has-hero.at-top .nav-desktop .button.text.active {
386
+ background-color: rgba(255, 255, 255, 0.15) !important;
387
+ color: #ffffff !important;
388
+ }
389
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.text.active:hover,
390
+ .top-nav.has-hero.at-top .nav-desktop .button.text.active:hover {
391
+ background-color: rgba(255, 255, 255, 0.25) !important;
392
+ }
393
+ .top-nav:not(.at-top) .nav-desktop .button.text.active {
394
+ background-color: var(--primary-container) !important;
395
+ color: var(--on-primary-container) !important;
396
+ }
397
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.border,
398
+ .top-nav.has-hero.at-top .nav-desktop .button.border {
336
399
  color: #f7f8f8;
337
400
  border-color: #f7f8f855;
338
401
  background: none;
339
402
  }
403
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.border.active,
404
+ .top-nav.has-hero.at-top .nav-desktop .button.border.active {
405
+ background-color: rgba(255, 255, 255, 0.15) !important;
406
+ color: #ffffff !important;
407
+ border-color: transparent !important;
408
+ }
409
+ .top-nav:not(.at-top) .nav-desktop .button.border.active {
410
+ background-color: var(--primary-container) !important;
411
+ color: var(--on-primary-container) !important;
412
+ border-color: transparent !important;
413
+ }
414
+ .nav-desktop menu > li.active,
415
+ .nav-desktop menu > li:has(> a.active) {
416
+ background-color: var(--primary-container) !important;
417
+ color: var(--on-primary-container) !important;
418
+ }
419
+ .nav-desktop menu > li.active > a,
420
+ .nav-desktop menu > li:has(> a.active) > a {
421
+ color: var(--on-primary-container) !important;
422
+ }
423
+ #mobile-menu .list li.active > a,
424
+ #mobile-menu .list li.active > span,
425
+ #mobile-menu .list summary:has(.active),
426
+ #mobile-menu .list summary.active {
427
+ color: var(--primary) !important;
428
+ font-weight: 600;
429
+ }
340
430
  .top-nav.at-top > nav > a:has(.logo.dark) > .logo.dark {
341
431
  display: none;
342
432
  }
343
- .Layout.layout-home .top-nav.at-top > nav > a:has(.logo.dark) > .logo.light {
433
+ .Layout.layout-home .top-nav.at-top > nav > a:has(.logo.dark) > .logo.light,
434
+ .top-nav.has-hero.at-top > nav > a:has(.logo.dark) > .logo.light {
344
435
  display: none;
345
436
  }
346
437
  .Layout.layout-home
347
438
  .top-nav.at-top
348
439
  > nav
349
440
  > a:not(:has(.logo.dark))
350
- > .logo.light {
441
+ > .logo.light,
442
+ .top-nav.has-hero.at-top > nav > a:not(:has(.logo.dark)) > .logo.light {
351
443
  filter: brightness(0) invert(1);
352
444
  }
353
445
  .Layout .top-nav > nav > a > .logo.dark {
354
446
  display: none;
355
447
  }
356
- .Layout.layout-home .top-nav.at-top > nav > a > .logo.dark {
448
+ .Layout.layout-home .top-nav.at-top > nav > a > .logo.dark,
449
+ .top-nav.has-hero.at-top > nav > a > .logo.dark {
357
450
  display: block;
358
451
  }
452
+ .top-nav > nav > a:has(.logo),
453
+ #mobile-menu nav > a:has(.logo) {
454
+ display: inline-flex;
455
+ align-items: center;
456
+ }
359
457
  img.logo {
360
- height: 32px;
458
+ height: var(--vp-nav-logo-height, 28px);
459
+ width: auto;
460
+ }
461
+ .top-nav img.logo {
462
+ height: var(--vp-nav-logo-height, 28px);
463
+ width: auto;
464
+ max-height: 100%;
465
+ object-fit: contain;
361
466
  }
362
467
  nav.nav-mobile {
363
468
  display: flex;
@@ -365,7 +470,7 @@ nav.nav-mobile {
365
470
  nav.nav-desktop {
366
471
  display: none;
367
472
  }
368
- @media (min-width: 768px) {
473
+ @media (min-width: 1024px) {
369
474
  nav.nav-mobile {
370
475
  display: none;
371
476
  }
@@ -373,6 +478,29 @@ nav.nav-desktop {
373
478
  display: flex;
374
479
  }
375
480
  }
481
+
482
+ /* Medium desktop viewports (1024px - 1200px): compact navigation styling to prevent overflow */
483
+ @media (min-width: 1024px) and (max-width: 1200px) {
484
+ .top-nav > nav.nav-desktop {
485
+ padding-left: 1rem !important;
486
+ padding-right: 1rem !important;
487
+ gap: 8px !important;
488
+ }
489
+ .nav-desktop > nav:not(.actions) > ul {
490
+ gap: 4px !important;
491
+ }
492
+ .nav-desktop .button.text {
493
+ padding: 0 8px !important;
494
+ font-size: 0.8125rem !important;
495
+ }
496
+ .nav-desktop .actions .button {
497
+ padding: 0 12px !important;
498
+ font-size: 0.8125rem !important;
499
+ }
500
+ .nav-desktop img.logo {
501
+ height: var(--vp-nav-logo-height, 28px) !important;
502
+ }
503
+ }
376
504
  details summary i.mdi-translate {
377
505
  transform: rotate(0deg) !important;
378
506
  }
@@ -1,6 +1,9 @@
1
1
  <template>
2
2
  <template v-for="item in items">
3
- <li v-if="item.items && item.text">
3
+ <li
4
+ v-if="item.items && item.text"
5
+ :class="{ active: isActive(item, route.path) }"
6
+ >
4
7
  <a
5
8
  v-if="item.link"
6
9
  :href="withBase(item.link)"
@@ -25,7 +28,7 @@
25
28
  </menu>
26
29
  </li>
27
30
  <NavDropdown v-else-if="item.items" :items="item.items" />
28
- <li v-else>
31
+ <li v-else :class="{ active: isActive(item, route.path) }">
29
32
  <a
30
33
  v-if="item.link"
31
34
  :href="withBase(item.link)"
@@ -60,3 +63,10 @@ defineProps({
60
63
  const route = useRoute();
61
64
  const { theme } = useData();
62
65
  </script>
66
+
67
+ <style scoped>
68
+ a,
69
+ a:hover {
70
+ text-decoration: none !important;
71
+ }
72
+ </style>