@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
@@ -30,7 +30,7 @@
30
30
  {{ formatDate(frontmatter.date) }}
31
31
  </time>
32
32
  </span>
33
-
33
+
34
34
  <!-- Author Span -->
35
35
  <span v-if="authors && authors.length" class="metadata-item">
36
36
  <i class="mdi mdi-account-outline metadata-icon"></i>
@@ -41,12 +41,14 @@
41
41
  class="author-item"
42
42
  >
43
43
  <a
44
- :href="`/author/${authorId}/`"
44
+ :href="withBase(`/author/${authorId}/`)"
45
45
  class="author-link-active"
46
46
  >
47
47
  {{ getAuthorName(authorId) }}
48
48
  </a>
49
- <span v-if="idx < authors.length - 1" class="author-separator">,</span>
49
+ <span v-if="idx < authors.length - 1" class="author-separator"
50
+ >,</span
51
+ >
50
52
  </span>
51
53
  </span>
52
54
  </div>
@@ -59,7 +61,7 @@
59
61
  <a
60
62
  v-for="tag in frontmatter.tags"
61
63
  :key="tag"
62
- :href="`/tag/${tag}/`"
64
+ :href="withBase(`/tag/${tag}/`)"
63
65
  :style="getTagStyle(tag)"
64
66
  class="post-tag"
65
67
  >
@@ -77,23 +79,18 @@
77
79
  <!-- Sidebar Column -->
78
80
  <div class="blog-sidebar-col">
79
81
  <!-- Table of Contents (On This Page) -->
80
- <div
81
- v-if="headers && headers.length"
82
- class="toc-container"
83
- >
82
+ <div v-if="headers && headers.length" class="toc-container">
84
83
  <VPDocAsideOutline />
85
84
  </div>
86
85
 
87
86
  <!-- Hot Tags -->
88
87
  <div class="sidebar-widget">
89
- <h3 class="sidebar-title">
90
- Hot tags
91
- </h3>
88
+ <h3 class="sidebar-title">Hot tags</h3>
92
89
  <div class="sidebar-tags-container">
93
90
  <a
94
91
  v-for="tag in calculatedHotTags"
95
92
  :key="tag.name"
96
- :href="`/tag/${tag.name}/`"
93
+ :href="withBase(`/tag/${tag.name}/`)"
97
94
  :style="getTagStyle(tag.name)"
98
95
  class="sidebar-tag"
99
96
  >
@@ -104,9 +101,7 @@
104
101
 
105
102
  <!-- Recent Posts -->
106
103
  <div class="sidebar-widget">
107
- <h3 class="sidebar-title">
108
- Recent posts
109
- </h3>
104
+ <h3 class="sidebar-title">Recent posts</h3>
110
105
  <div class="space-y-4">
111
106
  <div
112
107
  v-for="post in calculatedRecentPosts"
@@ -114,7 +109,7 @@
114
109
  class="flex flex-col gap-1 border-b border-slate-100 last:border-none pb-3 last:pb-0"
115
110
  >
116
111
  <a
117
- :href="post.url"
112
+ :href="withBase(post.url)"
118
113
  class="text-sm font-semibold text-slate-900 hover:text-primary leading-snug transition-colors line-clamp-2"
119
114
  >
120
115
  {{ post.title }}
@@ -131,7 +126,7 @@
131
126
  </template>
132
127
 
133
128
  <script setup>
134
- import { useData } from "vitepress";
129
+ import { useData, withBase } from "vitepress";
135
130
  import { computed } from "vue";
136
131
  import { data as posts } from "../posts.data.js";
137
132
  import { data as authorsList } from "../authors.data.js";
@@ -153,10 +148,17 @@ const headers = computed(() => {
153
148
  // Form cover image URL correctly (prefixing filename with /images/)
154
149
  function getCoverImageUrl(coverImage) {
155
150
  if (!coverImage) return "";
156
- if (coverImage.startsWith("/") || coverImage.startsWith("http")) {
151
+ if (coverImage.startsWith("http") || coverImage.startsWith("//")) {
152
+ return coverImage;
153
+ }
154
+ const base = withBase("/");
155
+ if (base && base !== "/" && coverImage.startsWith(base)) {
157
156
  return coverImage;
158
157
  }
159
- return `/images/${coverImage}`;
158
+ const path = coverImage.startsWith("/")
159
+ ? coverImage
160
+ : `/images/${coverImage}`;
161
+ return withBase(path);
160
162
  }
161
163
 
162
164
  // Format publication date
@@ -189,7 +191,19 @@ const coverImage = computed(() => {
189
191
 
190
192
  // Dynamic Recent Posts (excluding current page)
191
193
  const calculatedRecentPosts = computed(() => {
192
- return posts.filter((p) => p.url !== page.value.url).slice(0, 5);
194
+ const rel = page.value.relativePath || "";
195
+ const current = (
196
+ "/" + rel.replace(/(^|\/)index\.md$/, "$1").replace(/\.md$/, "")
197
+ )
198
+ .replace(/\/$/, "")
199
+ .replace(/\.html$/, "");
200
+
201
+ return posts
202
+ .filter((p) => {
203
+ const pUrl = (p.url || "").replace(/\/$/, "").replace(/\.html$/, "");
204
+ return pUrl !== current && p.title !== frontmatter.value.title;
205
+ })
206
+ .slice(0, 5);
193
207
  });
194
208
 
195
209
  // Dynamic Hot Tags
@@ -320,7 +334,9 @@ const calculatedHotTags = computed(() => {
320
334
  align-items: center;
321
335
  justify-content: center;
322
336
  height: 100%;
323
- transform: translateY(0.5px); /* Micro-adjustment for perfect vertical alignment */
337
+ transform: translateY(
338
+ 0.5px
339
+ ); /* Micro-adjustment for perfect vertical alignment */
324
340
  }
325
341
 
326
342
  .metadata-text {
@@ -343,7 +359,9 @@ const calculatedHotTags = computed(() => {
343
359
  font-weight: 600;
344
360
  color: var(--vp-c-brand, #004170) !important;
345
361
  text-decoration: none;
346
- transition: color 0.15s ease, text-decoration 0.15s ease;
362
+ transition:
363
+ color 0.15s ease,
364
+ text-decoration 0.15s ease;
347
365
  line-height: 1.2;
348
366
  }
349
367
 
@@ -391,7 +409,9 @@ const calculatedHotTags = computed(() => {
391
409
  border-radius: 4px !important;
392
410
  text-decoration: none !important;
393
411
  line-height: 1.2 !important;
394
- transition: opacity 0.2s ease, transform 0.1s ease !important;
412
+ transition:
413
+ opacity 0.2s ease,
414
+ transform 0.1s ease !important;
395
415
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
396
416
  }
397
417
 
@@ -416,7 +436,9 @@ const calculatedHotTags = computed(() => {
416
436
  border-radius: 4px !important;
417
437
  text-decoration: none !important;
418
438
  line-height: 1.2 !important;
419
- transition: opacity 0.2s ease, transform 0.1s ease !important;
439
+ transition:
440
+ opacity 0.2s ease,
441
+ transform 0.1s ease !important;
420
442
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
421
443
  }
422
444
 
@@ -4,7 +4,7 @@
4
4
  :class="
5
5
  dark !== undefined
6
6
  ? 'primary primary-gradient-bg'
7
- : 'surface-container-low'
7
+ : 'surface-container-lowest'
8
8
  "
9
9
  >
10
10
  <div class="large-space"></div>
@@ -18,7 +18,7 @@
18
18
  v-if="primaryButton"
19
19
  class="button extra small-margin responsive-mobile"
20
20
  :class="dark !== undefined ? 'surface' : 'primary'"
21
- :href="primaryLink"
21
+ :href="resolvedPrimaryLink"
22
22
  @click="
23
23
  trackEvent(['CTA', 'Click', 'CTA Section Primary', primaryButton])
24
24
  "
@@ -27,7 +27,7 @@
27
27
  <a
28
28
  v-if="secondaryButton"
29
29
  class="button extra small-margin secondary responsive-mobile"
30
- :href="secondaryLink"
30
+ :href="resolvedSecondaryLink"
31
31
  @click="
32
32
  trackEvent(['CTA', 'Click', 'CTA Section Secondary', secondaryButton])
33
33
  "
@@ -37,7 +37,7 @@
37
37
  v-if="altButton"
38
38
  class="button border extra small-margin responsive-mobile"
39
39
  :class="dark !== undefined ? 'white-text' : ''"
40
- :href="altLink"
40
+ :href="resolvedAltLink"
41
41
  @click="trackEvent(['CTA', 'Click', 'CTA Section Alt', altButton])"
42
42
  >{{ altButton }}</a
43
43
  >
@@ -49,6 +49,8 @@
49
49
  </template>
50
50
 
51
51
  <script setup>
52
+ import { computed } from "vue";
53
+ import { withBase } from "vitepress";
52
54
  import { trackEvent } from "../helpers";
53
55
  const props = defineProps([
54
56
  "dark",
@@ -61,6 +63,18 @@ const props = defineProps([
61
63
  "tagline",
62
64
  "title",
63
65
  ]);
66
+
67
+ function resolveLink(link) {
68
+ if (!link) return "";
69
+ if (link.startsWith("http") || link.startsWith("//")) return link;
70
+ const base = withBase("/");
71
+ if (base && base !== "/" && link.startsWith(base)) return link;
72
+ return link.startsWith("/") ? withBase(link) : withBase("/" + link);
73
+ }
74
+
75
+ const resolvedPrimaryLink = computed(() => resolveLink(props.primaryLink));
76
+ const resolvedSecondaryLink = computed(() => resolveLink(props.secondaryLink));
77
+ const resolvedAltLink = computed(() => resolveLink(props.altLink));
64
78
  </script>
65
79
 
66
80
  <style>
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
+ import { withBase } from "vitepress";
3
4
  import { data as authorsList } from "../../authors.data.js";
4
5
  import { getTagColor } from "../../tagUtils.js";
5
6
 
@@ -33,8 +34,8 @@ const props = defineProps({
33
34
  required: false,
34
35
  },
35
36
  icon: {
36
- /** @type {import('vue').PropType<{ html: string, width?: number, height?: number }>} */
37
- type: Object,
37
+ /** @type {import('vue').PropType<string | { html: string, width?: number, height?: number }>} */
38
+ type: [Object, String],
38
39
  required: false,
39
40
  },
40
41
  date: {
@@ -53,6 +54,22 @@ const props = defineProps({
53
54
  },
54
55
  });
55
56
 
57
+ function resolveUrl(url) {
58
+ if (!url) return "";
59
+ if (url.startsWith("http") || url.startsWith("//")) return url;
60
+ const base = withBase("/");
61
+ if (base && base !== "/" && url.startsWith(base)) return url;
62
+ return url.startsWith("/") ? withBase(url) : withBase("/" + url);
63
+ }
64
+
65
+ const resolvedImage = computed(() => resolveUrl(props.image));
66
+ const resolvedLinkHref = computed(() =>
67
+ props.link?.href ? resolveUrl(props.link.href) : undefined,
68
+ );
69
+ const resolvedSecondaryLinkHref = computed(() =>
70
+ props.secondaryLink?.href ? resolveUrl(props.secondaryLink.href) : undefined,
71
+ );
72
+
56
73
  const iconStyle = {
57
74
  width: (props.icon?.width ?? 40) + "px",
58
75
  height: (props.icon?.height ?? 40) + "px",
@@ -60,7 +77,10 @@ const iconStyle = {
60
77
 
61
78
  // Check if we should render a full-width cover image
62
79
  const isCoverLayout = computed(() => {
63
- return props.image && (props.variant === "cover" || props.date || props.authors.length);
80
+ return (
81
+ props.image &&
82
+ (props.variant === "cover" || props.date || props.authors.length)
83
+ );
64
84
  });
65
85
 
66
86
  // Check if the card itself is fully clickable (has a link but NO link text)
@@ -71,7 +91,7 @@ const isClickableCard = computed(() => {
71
91
  function getAuthorName(authorId) {
72
92
  if (!authorId) return "EOX Experts";
73
93
  const author = authorsList.find(
74
- (a) => a.id.toLowerCase().trim() === authorId.toLowerCase().trim()
94
+ (a) => a.id.toLowerCase().trim() === authorId.toLowerCase().trim(),
75
95
  );
76
96
  return author ? author.name : authorId;
77
97
  }
@@ -82,31 +102,31 @@ function getTagStyle(tag) {
82
102
  return {
83
103
  backgroundColor: "rgba(255, 94, 94, 0.15)",
84
104
  border: "1px solid rgb(255, 94, 94)",
85
- color: "#d32f2f"
105
+ color: "#d32f2f",
86
106
  };
87
107
  } else if (color.startsWith("rgb(0, 177, 176)")) {
88
108
  return {
89
109
  backgroundColor: "rgba(0, 177, 176, 0.15)",
90
110
  border: "1px solid rgb(0, 177, 176)",
91
- color: "#00796b"
111
+ color: "#00796b",
92
112
  };
93
113
  } else if (color.startsWith("rgb(219, 173, 66)")) {
94
114
  return {
95
115
  backgroundColor: "rgba(219, 173, 66, 0.15)",
96
116
  border: "1px solid rgb(219, 173, 66)",
97
- color: "#f57c00"
117
+ color: "#f57c00",
98
118
  };
99
119
  } else if (color.startsWith("rgb(218, 71, 228)")) {
100
120
  return {
101
121
  backgroundColor: "rgba(218, 71, 228, 0.15)",
102
122
  border: "1px solid rgb(218, 71, 228)",
103
- color: "#c2185b"
123
+ color: "#c2185b",
104
124
  };
105
125
  }
106
126
  return {
107
127
  backgroundColor: "rgba(0, 0, 0, 0.05)",
108
128
  border: "1px solid rgba(0, 0, 0, 0.1)",
109
- color: "#475569"
129
+ color: "#475569",
110
130
  };
111
131
  }
112
132
 
@@ -126,27 +146,64 @@ function formatDate(dateString) {
126
146
  class="vertical large-padding no-margin theme-feature-card"
127
147
  :class="[
128
148
  isClickableCard ? 'small-elevate clickable-card' : 'no-elevate',
129
- isCoverLayout ? 'has-cover-image' : ''
149
+ isCoverLayout ? 'has-cover-image' : '',
130
150
  ]"
131
151
  >
132
152
  <!-- 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" />
153
+ <a
154
+ v-if="isCoverLayout && resolvedLinkHref"
155
+ :href="resolvedLinkHref"
156
+ :target="link?.target ?? '_blank'"
157
+ class="card-cover-link"
158
+ >
159
+ <img
160
+ :src="resolvedImage"
161
+ alt=""
162
+ class="card-cover-image"
163
+ loading="lazy"
164
+ />
135
165
  </a>
166
+ <div v-else-if="isCoverLayout" class="card-cover-link">
167
+ <img
168
+ :src="resolvedImage"
169
+ alt=""
170
+ class="card-cover-image"
171
+ loading="lazy"
172
+ />
173
+ </div>
136
174
 
137
175
  <div class="card-content-wrapper">
138
176
  <i
139
177
  v-if="typeof icon === 'string' && icon.startsWith('mdi-')"
140
178
  :class="`mdi ${icon}`"
141
179
  ></i>
180
+ <img
181
+ v-else-if="typeof icon === 'string'"
182
+ :src="resolveUrl(icon)"
183
+ alt=""
184
+ class="icon"
185
+ style="
186
+ height: 36px;
187
+ width: auto;
188
+ max-width: 120px;
189
+ object-fit: contain;
190
+ margin-bottom: 0.75rem;
191
+ border-radius: 0 !important;
192
+ "
193
+ />
142
194
  <div
143
- v-else-if="icon"
195
+ v-else-if="icon && icon.html"
144
196
  :style="iconStyle"
145
197
  v-html="icon.html"
146
198
  class="icon"
147
199
  ></div>
148
200
  <!-- Render small icon image only if NOT rendering a full cover image -->
149
- <img v-else-if="image && !isCoverLayout" :src="image" alt="" class="icon" />
201
+ <img
202
+ v-else-if="image && !isCoverLayout"
203
+ :src="resolvedImage"
204
+ alt=""
205
+ class="icon"
206
+ />
150
207
 
151
208
  <!-- Metadata & Tags directly under place/author -->
152
209
  <div v-if="date || (authors && authors.length)" class="metadata-wrapper">
@@ -159,18 +216,33 @@ function formatDate(dateString) {
159
216
  <span class="author-icon">
160
217
  <i class="mdi mdi-account-outline"></i>
161
218
  </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>
219
+ <span
220
+ v-for="(authorId, idx) in authors"
221
+ :key="authorId"
222
+ class="author-item"
223
+ >
224
+ <a :href="withBase(`/author/${authorId}/`)" class="author-link">{{
225
+ getAuthorName(authorId)
226
+ }}</a
227
+ ><span v-if="idx < authors.length - 1" class="author-separator"
228
+ >,</span
229
+ >
164
230
  </span>
165
231
  </span>
166
232
  </div>
167
233
 
168
234
  <!-- Tags directly under metadata if chips are provided and this is a rich card -->
169
235
  <div v-if="chips && chips.length" class="blog-tags-container">
170
- <a
171
- v-for="chip in chips"
236
+ <a
237
+ v-for="chip in chips"
172
238
  :key="chip.text"
173
- :href="chip.link || chip.href || `/tag/${chip.text}/`"
239
+ :href="
240
+ chip.link
241
+ ? resolveUrl(chip.link)
242
+ : chip.href
243
+ ? resolveUrl(chip.href)
244
+ : withBase(`/tag/${chip.text}/`)
245
+ "
174
246
  :style="getTagStyle(chip.text)"
175
247
  class="tag-link"
176
248
  >
@@ -180,7 +252,7 @@ function formatDate(dateString) {
180
252
  </div>
181
253
 
182
254
  <h5 class="small card-title-text">
183
- <a v-if="link?.href" :href="link.href">{{ title }}</a>
255
+ <a v-if="resolvedLinkHref" :href="resolvedLinkHref">{{ title }}</a>
184
256
  <span v-else>{{ title }}</span>
185
257
  </h5>
186
258
 
@@ -193,7 +265,13 @@ function formatDate(dateString) {
193
265
  <template v-for="chip in chips" :key="chip.text">
194
266
  <a
195
267
  v-if="chip.link || chip.href"
196
- :href="chip.link || chip.href"
268
+ :href="
269
+ chip.link
270
+ ? resolveUrl(chip.link)
271
+ : chip.href
272
+ ? resolveUrl(chip.href)
273
+ : undefined
274
+ "
197
275
  :style="getTagStyle(chip.text)"
198
276
  class="chip small-chip border clickable-chip"
199
277
  :class="chip.class"
@@ -211,7 +289,10 @@ function formatDate(dateString) {
211
289
  </template>
212
290
  </div>
213
291
 
214
- <p v-if="metadata && metadata.length && !date && !authors.length" class="small-text">
292
+ <p
293
+ v-if="metadata && metadata.length && !date && !authors.length"
294
+ class="small-text"
295
+ >
215
296
  <span
216
297
  v-for="(item, index) in metadata"
217
298
  :key="index"
@@ -240,7 +321,7 @@ function formatDate(dateString) {
240
321
  >
241
322
  <a
242
323
  v-if="link && !!link.text"
243
- :href="link.href"
324
+ :href="resolvedLinkHref"
244
325
  :target="link.target ?? '_blank'"
245
326
  class="button transparent bold primary-text no-padding"
246
327
  >
@@ -251,7 +332,7 @@ function formatDate(dateString) {
251
332
  </a>
252
333
  <a
253
334
  v-if="secondaryLink"
254
- :href="secondaryLink.href"
335
+ :href="resolvedSecondaryLinkHref"
255
336
  :target="secondaryLink.target ?? '_blank'"
256
337
  class="button transparent primary-text no-padding secondary-link"
257
338
  >
@@ -295,13 +376,17 @@ function formatDate(dateString) {
295
376
  /* Clickable Cards Hover System */
296
377
  .theme-feature-card.clickable-card {
297
378
  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;
379
+ transition:
380
+ transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1),
381
+ box-shadow 0.3s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
299
382
  will-change: transform, box-shadow;
300
383
  }
301
384
 
302
385
  .theme-feature-card.clickable-card:hover {
303
386
  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;
387
+ box-shadow:
388
+ 0 12px 28px rgba(0, 0, 0, 0.12),
389
+ 0 4px 8px rgba(0, 0, 0, 0.04) !important;
305
390
  }
306
391
 
307
392
  .small-chip {
@@ -474,6 +559,8 @@ function formatDate(dateString) {
474
559
  }
475
560
 
476
561
  /* card title overrides to make sure it respects the 20px request */
562
+ h5.small.card-title-text,
563
+ .card-title-text,
477
564
  .VPHome .vp-doc h5.small.card-title-text {
478
565
  font-size: 20px !important;
479
566
  line-height: 1.4 !important;
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
+ import { withBase } from "vitepress";
3
4
 
4
5
  const props = defineProps({
5
6
  title: String,
@@ -41,6 +42,22 @@ const props = defineProps({
41
42
  },
42
43
  });
43
44
 
45
+ function resolveUrl(url) {
46
+ if (!url) return "";
47
+ if (url.startsWith("http") || url.startsWith("//")) return url;
48
+ const base = withBase("/");
49
+ if (base && base !== "/" && url.startsWith(base)) return url;
50
+ return url.startsWith("/") ? withBase(url) : withBase("/" + url);
51
+ }
52
+
53
+ const resolvedImage = computed(() => resolveUrl(props.image));
54
+ const resolvedLinkHref = computed(() =>
55
+ props.link?.href ? resolveUrl(props.link.href) : undefined,
56
+ );
57
+ const resolvedSecondaryLinkHref = computed(() =>
58
+ props.secondaryLink?.href ? resolveUrl(props.secondaryLink.href) : undefined,
59
+ );
60
+
44
61
  const cardStyle = computed(() => ({
45
62
  minHeight: props.minHeight,
46
63
  }));
@@ -52,7 +69,12 @@ const cardStyle = computed(() => ({
52
69
  :class="link && !link.text ? 'small-elevate' : 'no-elevate'"
53
70
  :style="cardStyle"
54
71
  >
55
- <img v-if="image" :src="image" alt="" class="image-overlay-card-image" />
72
+ <img
73
+ v-if="image"
74
+ :src="resolvedImage"
75
+ alt=""
76
+ class="image-overlay-card-image"
77
+ />
56
78
  <div class="image-overlay-card-overlay"></div>
57
79
  <div class="image-overlay-card-content padding">
58
80
  <div style="padding-top: 3rem">
@@ -103,7 +125,7 @@ const cardStyle = computed(() => ({
103
125
  <a
104
126
  v-if="link && link.text"
105
127
  class="button small white black-text"
106
- :href="link.href"
128
+ :href="resolvedLinkHref"
107
129
  :target="link.target ?? '_blank'"
108
130
  >
109
131
  {{ link.text }}
@@ -111,7 +133,7 @@ const cardStyle = computed(() => ({
111
133
  <a
112
134
  v-if="secondaryLink"
113
135
  class="button small border white-text secondary-link"
114
- :href="secondaryLink.href"
136
+ :href="resolvedSecondaryLinkHref"
115
137
  :target="secondaryLink.target ?? '_blank'"
116
138
  >
117
139
  {{ secondaryLink.text }}
@@ -1,3 +1,4 @@
1
+ import { h } from "vue";
1
2
  import FeatureSection from "./FeatureSection.vue";
2
3
 
3
4
  describe("<FeatureSection />", () => {
@@ -60,4 +61,19 @@ describe("<FeatureSection />", () => {
60
61
  cy.get(".feature-section").should("have.class", "dark");
61
62
  cy.get(".feature-section").should("have.class", "reverse");
62
63
  });
64
+
65
+ it("has padding-block-start 0 when following a title", () => {
66
+ const TestWrapper = {
67
+ render() {
68
+ return h("div", [
69
+ h("h2", "Products & Services"),
70
+ h("p", "Subtitle paragraph"),
71
+ h(FeatureSection, defaultProps),
72
+ ]);
73
+ },
74
+ };
75
+
76
+ cy.mount(TestWrapper);
77
+ cy.get(".feature-section").should("have.css", "padding-top", "0px");
78
+ });
63
79
  });
@@ -120,16 +120,11 @@ const props = defineProps([
120
120
  }
121
121
  .feature-section.landing .holder {
122
122
  padding: 32px 0;
123
- background: var(--surface-container-low);
123
+ background: var(--surface-container-lowest, #ffffff);
124
124
  }
125
125
  .feature-section.dark .holder {
126
- background: var(--primary);
127
- background: radial-gradient(
128
- 74.48% 130.95% at 50% -12.27%,
129
- hsl(from var(--primary) h s calc(l + 7)) 0%,
130
- var(--primary) 45%,
131
- hsl(from var(--primary) h s calc(l - 20)) 100%
132
- );
126
+ background: #000c14;
127
+ background: linear-gradient(180deg, var(--primary, #004170) 0%, #000c14 100%);
133
128
  color: #fff;
134
129
  }
135
130
  .feature-section nav {