@eox/pages-theme-eox 2.0.0-alpha.7 → 2.0.0-alpha.8
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.
- package/README.md +21 -46
- package/cypress/support/mocks/helpers.js +25 -3
- package/package.json +3 -4
- package/src/Layout.vue +6 -3
- package/src/components/BlogList.vue +38 -25
- package/src/components/CTASection.vue +1 -1
- package/src/components/FeatureCard/Index.vue +16 -1
- package/src/components/FeatureSection.vue +3 -8
- package/src/components/FeaturesGallery.vue +5 -0
- package/src/components/Footer.cy.js +40 -2
- package/src/components/Footer.vue +5 -3
- package/src/components/HeroSection.vue +103 -15
- package/src/components/LogoSection.vue +1 -0
- package/src/components/MobileNavDropdown.vue +6 -3
- package/src/components/NavBar.vue +95 -15
- package/src/components/NavDropdown.vue +5 -2
- package/src/components/TagList.vue +16 -3
- package/src/components/TeamPage.vue +13 -3
- package/src/components/TestimonialsSection.vue +119 -16
- package/src/components/Tutorial.vue +3 -2
- package/src/helpers.js +13 -5
- package/src/helpers.test.js +28 -0
- package/src/pages/author/[author].md +1 -0
- package/src/pages/blog.md +1 -0
- package/src/pages/tag/[tag].md +1 -0
- package/src/pages/tag.md +1 -0
- package/src/pages/team.md +1 -0
- package/src/style.css +19 -8
- package/src/vitepressConfig.mjs +0 -6
- package/src/llms/index.js +0 -64
- package/src/llms/llms.test.js +0 -980
- package/src/llms/storiesPlugin.js +0 -771
package/README.md
CHANGED
|
@@ -21,45 +21,6 @@ See also the [Vitepress Docs](https://vitepress.dev/guide/custom-theme#consuming
|
|
|
21
21
|
|
|
22
22
|
This theme supports standard [VitePress Default Theme Configuration](https://vitepress.dev/reference/default-theme-config) with some specific adaptations.
|
|
23
23
|
|
|
24
|
-
### LLMs Configuration (`llms.txt`)
|
|
25
|
-
|
|
26
|
-
This theme integrates [vitepress-plugin-llms](https://github.com/nmoodev/vitepress-plugin-llms) to automatically generate AI-friendly documentation (`llms.txt` and `llms-full.txt`), along with custom support for VitePress story routes and remote story manifests.
|
|
27
|
-
|
|
28
|
-
Generation is enabled by default. You can configure options via `brandConfig.llms` in your brand configuration:
|
|
29
|
-
|
|
30
|
-
```javascript
|
|
31
|
-
// Example brandConfig:
|
|
32
|
-
export const config = {
|
|
33
|
-
// ... other brand settings ...
|
|
34
|
-
llms: {
|
|
35
|
-
// Upstream vitepress-plugin-llms options
|
|
36
|
-
domain: "https://yourdomain.com", // Base URL to resolve absolute links in llms.txt
|
|
37
|
-
copyButtons: true, // (Optional) Adds copy buttons to code blocks in documentation
|
|
38
|
-
|
|
39
|
-
// Theme-specific Story Routes config
|
|
40
|
-
stories: {
|
|
41
|
-
heading: "Stories", // (Optional) Heading used in llms.txt and route pages (default: "Stories")
|
|
42
|
-
path: "narratives/[narrative]", // (Optional) Story route pattern (e.g. "narratives/[narrative]" or "story"). Also accepts `storyPath`
|
|
43
|
-
manifest: "https://cdn.example.com/narratives.json", // (Optional) URL to stories manifest (auto-detected from gallery components if omitted)
|
|
44
|
-
concurrency: 8, // (Optional) Maximum concurrent story downloads (default: 8)
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
#### Disabling LLMs
|
|
51
|
-
|
|
52
|
-
- To disable LLM documentation generation entirely: set `llms: false`.
|
|
53
|
-
- To generate general docs without stories: set `stories: false` inside `llms`:
|
|
54
|
-
|
|
55
|
-
```javascript
|
|
56
|
-
export const config = {
|
|
57
|
-
llms: {
|
|
58
|
-
stories: false,
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
```
|
|
62
|
-
|
|
63
24
|
### Navigation Bar
|
|
64
25
|
|
|
65
26
|
The Navigation Bar follows the [Default Theme Nav](https://vitepress.dev/reference/default-theme-nav) structure with the following differences:
|
|
@@ -184,26 +145,40 @@ The theme provides several built-in components and layout features. Some are dri
|
|
|
184
145
|
|
|
185
146
|
### Hero Section
|
|
186
147
|
|
|
187
|
-
To add a Hero section to the top of a page, use the `hero` frontmatter object.
|
|
148
|
+
To add a Hero section to the top of a page, use the `hero` frontmatter object. Heros feature a dark gradient transitioning from **DARK (top) to LIGHT (bottom)** (`.hero-gradient-bg`), allowing the top navigation bar to seamlessly sit transparently over the hero. On the landing page, the hero is larger and incorporates an isometric line pattern.
|
|
188
149
|
|
|
189
150
|
```yaml
|
|
190
151
|
---
|
|
191
152
|
hero:
|
|
192
|
-
title: "Welcome to My Site"
|
|
193
|
-
tagline: "Building the future of
|
|
153
|
+
text: "Welcome to My Site" # or title: "Welcome to My Site"
|
|
154
|
+
tagline: "Building the future of geospatial engineering"
|
|
194
155
|
image:
|
|
195
156
|
src: "/hero-image.png"
|
|
196
157
|
alt: "Hero Image"
|
|
197
158
|
actions:
|
|
198
159
|
- text: "Get Started"
|
|
199
160
|
link: "/start"
|
|
200
|
-
|
|
161
|
+
theme: "brand" # "brand" (primary surface), "secondary", or "alt" (bordered)
|
|
201
162
|
- text: "Learn More"
|
|
202
163
|
link: "/about"
|
|
203
|
-
|
|
164
|
+
theme: "secondary"
|
|
204
165
|
---
|
|
205
166
|
```
|
|
206
167
|
|
|
168
|
+
#### Hero on Aggregator Pages (`blog`, `team`, `tags`)
|
|
169
|
+
|
|
170
|
+
Pages using aggregator layouts (`layout: team`, `layout: blog`, or `layout: tags`) support optional heroes:
|
|
171
|
+
|
|
172
|
+
- **Without Hero (when `hero` is omitted or `hero: false`)**: The page renders a standard in-page header title beneath the solid top navigation bar.
|
|
173
|
+
- **With Hero (`hero: true` or custom `hero: { text, tagline, ... }`)**: The page renders the full dark hero with the `#000C14` → `#004170` gradient spanning seamlessly behind the transparent navigation bar. Setting `hero: true` automatically supplies appropriate layout titles (including dynamic `Tag: <tag>` and `Author: <author>` for filtered blog listings). Custom text, taglines, images, and actions can also be provided via the `hero` frontmatter object.
|
|
174
|
+
|
|
175
|
+
#### Gradient Design Standards
|
|
176
|
+
|
|
177
|
+
The theme standardizes two distinct gradient directions for dark visual elements using `#000C14` and `#004170`:
|
|
178
|
+
|
|
179
|
+
- **Hero Gradient (`.hero-gradient-bg`)**: Always transitions from **DARK (top) to LIGHT (bottom)**, running from `#000C14` (top) to `#004170` (bottom). Used for the landing page hero and all sub-page heroes. This anchors the fixed header area with dark contrast under navigation links while flowing naturally into lighter content below.
|
|
180
|
+
- **Dark Sections Gradient (`.primary-gradient-bg` / `.dark-section-gradient-bg`)**: Used in intermediate dark page sections further down the page (such as `<CTASection dark>`, feature highlights, or dark showcase galleries). Always transitions from **LIGHT (top) to DARK (bottom)**, running from `#004170` (top) to `#000C14` (bottom).
|
|
181
|
+
|
|
207
182
|
### Feature Section
|
|
208
183
|
|
|
209
184
|
Use the `<FeatureSection>` component to highlight specific features.
|
|
@@ -262,7 +237,7 @@ A flexible pricing table component.
|
|
|
262
237
|
|
|
263
238
|
#### CTA Section
|
|
264
239
|
|
|
265
|
-
A Call-to-Action section typically placed at the bottom of a page to encourage user engagement.
|
|
240
|
+
A Call-to-Action section typically placed at the bottom of a page to encourage user engagement. By default, it renders with a clean white (`surface-container-lowest`) background, or a dark gradient when the `dark` prop is passed.
|
|
266
241
|
|
|
267
242
|
```html
|
|
268
243
|
<CTASection
|
|
@@ -373,7 +348,7 @@ A standard content card with an icon or image, title, optional chips, metadata,
|
|
|
373
348
|
section-title="All Features"
|
|
374
349
|
variant="default"
|
|
375
350
|
columns="3/2/1"
|
|
376
|
-
background="surface-container-
|
|
351
|
+
background="surface-container-lowest"
|
|
377
352
|
:cards="[
|
|
378
353
|
{
|
|
379
354
|
id: 1,
|
|
@@ -48,9 +48,31 @@ window.__helpersMock = {
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
isActive: (item, path) => {
|
|
51
|
-
if (item
|
|
52
|
-
if (item.
|
|
53
|
-
|
|
51
|
+
if (!item || !path) return false;
|
|
52
|
+
if (item.activeMatch) {
|
|
53
|
+
try {
|
|
54
|
+
return new RegExp(item.activeMatch).test(path);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
item.items &&
|
|
61
|
+
item.items.some((sub) => window.__helpersMock.isActive(sub, path))
|
|
62
|
+
) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
if (item.link) {
|
|
66
|
+
const normalize = (p) =>
|
|
67
|
+
decodeURI(p)
|
|
68
|
+
.replace(/[?#].*$/, "")
|
|
69
|
+
.replace(/\.html$/, "")
|
|
70
|
+
.replace(/\/$/, "");
|
|
71
|
+
const linkPath = normalize(item.link);
|
|
72
|
+
const currentPath = normalize(path);
|
|
73
|
+
if (linkPath === currentPath) return true;
|
|
74
|
+
if (linkPath === "") return currentPath === "";
|
|
75
|
+
return currentPath === linkPath || currentPath.startsWith(linkPath + "/");
|
|
54
76
|
}
|
|
55
77
|
return false;
|
|
56
78
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eox/pages-theme-eox",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vitepress Theme with EOX branding",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"format": "npx prettier --write .",
|
|
14
14
|
"format:check": "npx prettier --check .",
|
|
15
15
|
"test": "npm run test:unit && npm run test:component",
|
|
16
|
-
"test:unit": "node --test src/helpers.test.js
|
|
16
|
+
"test:unit": "node --test src/helpers.test.js",
|
|
17
17
|
"test:component": "cypress run --component",
|
|
18
18
|
"release": "release-it"
|
|
19
19
|
},
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
"@eox/eslint-config": "^2.0.0",
|
|
22
22
|
"@eox/feedback": "^1.1.1",
|
|
23
23
|
"@eox/ui": "^1.0.1",
|
|
24
|
-
"vitepress": "^1.6.3"
|
|
25
|
-
"vitepress-plugin-llms": "^1.13.5"
|
|
24
|
+
"vitepress": "^1.6.3"
|
|
26
25
|
},
|
|
27
26
|
"devDependencies": {
|
|
28
27
|
"@release-it/conventional-changelog": "^10.0.6",
|
package/src/Layout.vue
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
v-if="frontmatter.layout === 'blog'"
|
|
4
|
-
class="layout-blog min-h-screen flex flex-col"
|
|
4
|
+
class="Layout layout-blog min-h-screen flex flex-col"
|
|
5
5
|
>
|
|
6
6
|
<NavBar />
|
|
7
7
|
<NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
|
|
8
|
+
<HeroSection />
|
|
8
9
|
<main class="flex-grow">
|
|
9
10
|
<BlogList />
|
|
10
11
|
</main>
|
|
@@ -13,10 +14,11 @@
|
|
|
13
14
|
</div>
|
|
14
15
|
<div
|
|
15
16
|
v-else-if="frontmatter.layout === 'tags'"
|
|
16
|
-
class="layout-tags min-h-screen flex flex-col"
|
|
17
|
+
class="Layout layout-tags min-h-screen flex flex-col"
|
|
17
18
|
>
|
|
18
19
|
<NavBar />
|
|
19
20
|
<NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
|
|
21
|
+
<HeroSection />
|
|
20
22
|
<main class="flex-grow">
|
|
21
23
|
<TagList />
|
|
22
24
|
</main>
|
|
@@ -25,10 +27,11 @@
|
|
|
25
27
|
</div>
|
|
26
28
|
<div
|
|
27
29
|
v-else-if="frontmatter.layout === 'team'"
|
|
28
|
-
class="layout-team min-h-screen flex flex-col"
|
|
30
|
+
class="Layout layout-team min-h-screen flex flex-col"
|
|
29
31
|
>
|
|
30
32
|
<NavBar />
|
|
31
33
|
<NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
|
|
34
|
+
<HeroSection />
|
|
32
35
|
<main class="flex-grow">
|
|
33
36
|
<TeamPage />
|
|
34
37
|
</main>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="blog-landing max-w-[1200px] mx-auto px-6 md:px-8"
|
|
4
|
+
:class="{ 'has-hero': Boolean(frontmatter.hero) }"
|
|
5
|
+
>
|
|
6
|
+
<!-- Page Header (shown only when hero is not present) -->
|
|
7
|
+
<div v-if="!frontmatter.hero" class="blog-landing-header" role="banner">
|
|
5
8
|
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
|
|
6
9
|
<div>
|
|
7
10
|
<h1 class="blog-landing-title">EOX Blog</h1>
|
|
@@ -10,28 +13,28 @@
|
|
|
10
13
|
</p>
|
|
11
14
|
</div>
|
|
12
15
|
</div>
|
|
16
|
+
</div>
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</div>
|
|
18
|
+
<!-- Active Filter Banner -->
|
|
19
|
+
<div v-if="activeTag || activeAuthor" class="active-filter-banner mb-6">
|
|
20
|
+
<span class="filter-label">Active filter:</span>
|
|
21
|
+
<span
|
|
22
|
+
v-if="activeTag"
|
|
23
|
+
:style="getTagStyle(activeTag)"
|
|
24
|
+
class="active-filter-tag shadow-sm inline-flex items-center gap-1.5"
|
|
25
|
+
>
|
|
26
|
+
{{ activeTag }}
|
|
27
|
+
</span>
|
|
28
|
+
<span
|
|
29
|
+
v-if="activeAuthor"
|
|
30
|
+
class="active-filter-author shadow-sm inline-flex items-center gap-1.5"
|
|
31
|
+
>
|
|
32
|
+
Author: {{ getAuthorName(activeAuthor) }}
|
|
33
|
+
</span>
|
|
34
|
+
<button @click="clearFilters" class="clear-filter-btn">
|
|
35
|
+
<i class="mdi mdi-close-circle"></i>
|
|
36
|
+
<span>Clear Filter</span>
|
|
37
|
+
</button>
|
|
35
38
|
</div>
|
|
36
39
|
|
|
37
40
|
<!-- Grid of Blog Posts -->
|
|
@@ -195,7 +198,7 @@ const calculatedHotTags = computed(() => {
|
|
|
195
198
|
});
|
|
196
199
|
|
|
197
200
|
const router = useRouter();
|
|
198
|
-
const { params } = useData();
|
|
201
|
+
const { params, frontmatter } = useData();
|
|
199
202
|
|
|
200
203
|
const activeTag = ref("");
|
|
201
204
|
const activeAuthor = ref("");
|
|
@@ -345,6 +348,10 @@ function clearFilters() {
|
|
|
345
348
|
padding-bottom: 48px !important;
|
|
346
349
|
}
|
|
347
350
|
|
|
351
|
+
.blog-landing.has-hero {
|
|
352
|
+
padding-top: 2rem !important;
|
|
353
|
+
}
|
|
354
|
+
|
|
348
355
|
@media (min-width: 640px) {
|
|
349
356
|
.blog-landing {
|
|
350
357
|
padding-left: 1.5rem !important;
|
|
@@ -352,6 +359,9 @@ function clearFilters() {
|
|
|
352
359
|
padding-top: calc(var(--vp-nav-height) + 40px) !important;
|
|
353
360
|
padding-bottom: 64px !important;
|
|
354
361
|
}
|
|
362
|
+
.blog-landing.has-hero {
|
|
363
|
+
padding-top: 2.5rem !important;
|
|
364
|
+
}
|
|
355
365
|
}
|
|
356
366
|
|
|
357
367
|
@media (min-width: 960px) {
|
|
@@ -361,6 +371,9 @@ function clearFilters() {
|
|
|
361
371
|
padding-top: calc(var(--vp-nav-height) + 58px) !important;
|
|
362
372
|
padding-bottom: 96px !important;
|
|
363
373
|
}
|
|
374
|
+
.blog-landing.has-hero {
|
|
375
|
+
padding-top: 3rem !important;
|
|
376
|
+
}
|
|
364
377
|
}
|
|
365
378
|
|
|
366
379
|
.blog-landing-header {
|
|
@@ -150,7 +150,12 @@ function formatDate(dateString) {
|
|
|
150
150
|
]"
|
|
151
151
|
>
|
|
152
152
|
<!-- Cover Image for cover/blog/rich cards -->
|
|
153
|
-
<a
|
|
153
|
+
<a
|
|
154
|
+
v-if="isCoverLayout && resolvedLinkHref"
|
|
155
|
+
:href="resolvedLinkHref"
|
|
156
|
+
:target="link?.target ?? '_blank'"
|
|
157
|
+
class="card-cover-link"
|
|
158
|
+
>
|
|
154
159
|
<img
|
|
155
160
|
:src="resolvedImage"
|
|
156
161
|
alt=""
|
|
@@ -158,6 +163,14 @@ function formatDate(dateString) {
|
|
|
158
163
|
loading="lazy"
|
|
159
164
|
/>
|
|
160
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>
|
|
161
174
|
|
|
162
175
|
<div class="card-content-wrapper">
|
|
163
176
|
<i
|
|
@@ -532,6 +545,8 @@ function formatDate(dateString) {
|
|
|
532
545
|
}
|
|
533
546
|
|
|
534
547
|
/* card title overrides to make sure it respects the 20px request */
|
|
548
|
+
h5.small.card-title-text,
|
|
549
|
+
.card-title-text,
|
|
535
550
|
.VPHome .vp-doc h5.small.card-title-text {
|
|
536
551
|
font-size: 20px !important;
|
|
537
552
|
line-height: 1.4 !important;
|
|
@@ -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-
|
|
123
|
+
background: var(--surface-container-lowest, #ffffff);
|
|
124
124
|
}
|
|
125
125
|
.feature-section.dark .holder {
|
|
126
|
-
background:
|
|
127
|
-
background:
|
|
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 {
|
|
@@ -50,6 +50,7 @@ const { cards, sectionTitle, background, variant, columns } = defineProps({
|
|
|
50
50
|
|
|
51
51
|
const cardComponents = {
|
|
52
52
|
default: FeatureCardDefault,
|
|
53
|
+
cover: FeatureCardDefault,
|
|
53
54
|
poster: defineAsyncComponent(() => import("./FeatureCard/Poster.vue")),
|
|
54
55
|
};
|
|
55
56
|
|
|
@@ -74,6 +75,8 @@ function resolveUrl(url) {
|
|
|
74
75
|
function normalizeFeature(feature) {
|
|
75
76
|
return {
|
|
76
77
|
...feature,
|
|
78
|
+
content: feature.content || feature.description,
|
|
79
|
+
chips: feature.chips || feature.tags,
|
|
77
80
|
image: resolveUrl(feature.image),
|
|
78
81
|
link: feature.link
|
|
79
82
|
? {
|
|
@@ -172,6 +175,7 @@ const gridCols = computed(() => {
|
|
|
172
175
|
:metadata="feature.metadata"
|
|
173
176
|
:link="feature.link"
|
|
174
177
|
:secondaryLink="feature.secondaryLink"
|
|
178
|
+
:variant="variant"
|
|
175
179
|
:class="`${cardBackground}`"
|
|
176
180
|
/>
|
|
177
181
|
</div>
|
|
@@ -186,6 +190,7 @@ const gridCols = computed(() => {
|
|
|
186
190
|
:metadata="feature.metadata"
|
|
187
191
|
:link="feature.link"
|
|
188
192
|
:secondaryLink="feature.secondaryLink"
|
|
193
|
+
:variant="variant"
|
|
189
194
|
:class="`${cardBackground}`"
|
|
190
195
|
/>
|
|
191
196
|
</div>
|
|
@@ -42,7 +42,7 @@ describe("<Footer />", () => {
|
|
|
42
42
|
|
|
43
43
|
it("renders legal links", () => {
|
|
44
44
|
cy.mount(Footer);
|
|
45
|
-
cy.contains("a", "
|
|
45
|
+
cy.contains("a", "Imprint").should("have.attr", "href", "/about");
|
|
46
46
|
cy.contains("a", "Terms & Conditions").should(
|
|
47
47
|
"have.attr",
|
|
48
48
|
"href",
|
|
@@ -51,6 +51,43 @@ describe("<Footer />", () => {
|
|
|
51
51
|
cy.contains("a", "Privacy").should("have.attr", "href", "/privacy");
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
+
it("renders uncategorized nav links under site title", () => {
|
|
55
|
+
cy.mount(Footer);
|
|
56
|
+
cy.contains("p.bold", "EOX Site").should("exist");
|
|
57
|
+
cy.contains("a", "Link 1").should("have.attr", "href", "/link1");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("renders uncategorized nav links under brandConfig title when present", () => {
|
|
61
|
+
__setMockData({
|
|
62
|
+
theme: {
|
|
63
|
+
theme: {
|
|
64
|
+
brandConfig: {
|
|
65
|
+
title: "Custom Brand",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
cy.mount(Footer);
|
|
71
|
+
cy.contains("p.bold", "Custom Brand").should("exist");
|
|
72
|
+
cy.contains("a", "Link 1").should("have.attr", "href", "/link1");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("renders dropdown items under category title", () => {
|
|
76
|
+
__setMockData({
|
|
77
|
+
theme: {
|
|
78
|
+
nav: [
|
|
79
|
+
{
|
|
80
|
+
text: "Our Company",
|
|
81
|
+
items: [{ text: "About", link: "/about" }],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
cy.mount(Footer);
|
|
87
|
+
cy.contains("p.bold", "Our Company").should("exist");
|
|
88
|
+
cy.contains("a", "About").should("have.attr", "href", "/about");
|
|
89
|
+
});
|
|
90
|
+
|
|
54
91
|
it("renders footer message with HTML content", () => {
|
|
55
92
|
__setMockData({
|
|
56
93
|
site: { title: "EOX Site" },
|
|
@@ -87,6 +124,7 @@ describe("<Footer />", () => {
|
|
|
87
124
|
},
|
|
88
125
|
i18n: {
|
|
89
126
|
About: "Über uns",
|
|
127
|
+
Imprint: "Impressum",
|
|
90
128
|
Legal: "Rechtliches",
|
|
91
129
|
"Powered by": "Unterstützt von",
|
|
92
130
|
},
|
|
@@ -96,7 +134,7 @@ describe("<Footer />", () => {
|
|
|
96
134
|
},
|
|
97
135
|
});
|
|
98
136
|
cy.mount(Footer);
|
|
99
|
-
cy.contains("
|
|
137
|
+
cy.contains("Impressum").should("exist");
|
|
100
138
|
cy.contains("Rechtliches").should("exist");
|
|
101
139
|
});
|
|
102
140
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<footer class="surface-container-
|
|
2
|
+
<footer class="surface-container-lowest row center-align">
|
|
3
3
|
<div class="holder large-padding">
|
|
4
4
|
<div class="large-space"></div>
|
|
5
5
|
<div class="grid">
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
class="footer-column-item"
|
|
37
37
|
v-if="theme.nav.filter((i) => !i.action && i.link).length"
|
|
38
38
|
>
|
|
39
|
-
<p class="bold">
|
|
39
|
+
<p class="bold">
|
|
40
|
+
{{ theme.theme?.brandConfig?.title || site.title }}
|
|
41
|
+
</p>
|
|
40
42
|
<p v-for="item in theme.nav.filter((i) => !i.action && i.link)">
|
|
41
43
|
<a
|
|
42
44
|
:href="withBase(item.link)"
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
"
|
|
73
75
|
target="_blank"
|
|
74
76
|
class="link"
|
|
75
|
-
>{{ t("
|
|
77
|
+
>{{ t("Imprint", theme.i18n) }}</a
|
|
76
78
|
>
|
|
77
79
|
</p>
|
|
78
80
|
<p v-for="(term, index) in termsLinks" :key="term.id || index">
|
|
@@ -1,40 +1,45 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<header v-if="
|
|
2
|
+
<header v-if="heroData" class="primary hero-gradient-bg">
|
|
3
3
|
<div
|
|
4
|
-
:class="`large-padding hero-container ${
|
|
4
|
+
:class="`large-padding hero-container ${heroData.image ? 'image' : ''}`"
|
|
5
5
|
>
|
|
6
6
|
<iframe
|
|
7
|
-
v-if="
|
|
7
|
+
v-if="heroData.image?.src?.includes('youtube')"
|
|
8
8
|
class="hero-image large-elevate small-round"
|
|
9
9
|
style="grid-area: image; aspect-ratio: 16/9"
|
|
10
|
-
:src="
|
|
10
|
+
:src="heroData.image.src"
|
|
11
11
|
frameborder="0"
|
|
12
12
|
allowfullscreen
|
|
13
13
|
/>
|
|
14
14
|
<video
|
|
15
|
-
v-else-if="
|
|
15
|
+
v-else-if="heroData.video"
|
|
16
16
|
class="hero-image large-elevate small-round"
|
|
17
|
-
:src="withBase(
|
|
17
|
+
:src="withBase(heroData.video.src)"
|
|
18
18
|
style="grid-area: image; aspect-ratio: 16/9"
|
|
19
19
|
autoplay
|
|
20
20
|
muted
|
|
21
21
|
loop
|
|
22
22
|
/>
|
|
23
23
|
<img
|
|
24
|
-
v-else-if="
|
|
24
|
+
v-else-if="heroData.image"
|
|
25
25
|
class="hero-image large-elevate small-round"
|
|
26
26
|
style="grid-area: image"
|
|
27
|
-
:src="withBase(
|
|
27
|
+
:src="withBase(heroData.image.src)"
|
|
28
|
+
:alt="heroData.image.alt || heroData.text"
|
|
28
29
|
/>
|
|
29
30
|
<div class="title" style="grid-area: title">
|
|
30
31
|
<h1 class="bold" style="font-size: clamp(2.25rem, 5vw, 3.3rem)">
|
|
31
|
-
{{
|
|
32
|
+
{{ heroData.text }}
|
|
32
33
|
</h1>
|
|
33
|
-
<p v-html="
|
|
34
|
+
<p v-if="heroData.tagline" v-html="heroData.tagline"></p>
|
|
34
35
|
</div>
|
|
35
|
-
<div
|
|
36
|
+
<div
|
|
37
|
+
v-if="heroData.actions && heroData.actions.length > 0"
|
|
38
|
+
style="grid-area: actions"
|
|
39
|
+
>
|
|
36
40
|
<a
|
|
37
|
-
v-for="(action, i) in
|
|
41
|
+
v-for="(action, i) in heroData.actions"
|
|
42
|
+
:key="i"
|
|
38
43
|
:href="withBase(action.link)"
|
|
39
44
|
:target="action.target"
|
|
40
45
|
:rel="action.rel"
|
|
@@ -62,17 +67,100 @@
|
|
|
62
67
|
</div>
|
|
63
68
|
</div>
|
|
64
69
|
<img
|
|
65
|
-
v-if="
|
|
70
|
+
v-if="heroData.background"
|
|
66
71
|
class="background-image"
|
|
67
|
-
:src="withBase(
|
|
72
|
+
:src="withBase(heroData.background.src)"
|
|
73
|
+
:alt="heroData.background.alt || ''"
|
|
68
74
|
/>
|
|
69
75
|
</header>
|
|
70
76
|
</template>
|
|
71
77
|
|
|
72
78
|
<script setup>
|
|
79
|
+
import { computed } from "vue";
|
|
73
80
|
import { useData, withBase } from "vitepress";
|
|
74
81
|
import { trackEvent } from "../helpers";
|
|
75
|
-
|
|
82
|
+
|
|
83
|
+
const props = defineProps({
|
|
84
|
+
text: String,
|
|
85
|
+
title: String,
|
|
86
|
+
tagline: String,
|
|
87
|
+
actions: Array,
|
|
88
|
+
image: Object,
|
|
89
|
+
video: Object,
|
|
90
|
+
background: Object,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const { frontmatter, params } = useData();
|
|
94
|
+
|
|
95
|
+
const heroData = computed(() => {
|
|
96
|
+
if (props.text || props.title || props.tagline) {
|
|
97
|
+
return {
|
|
98
|
+
text: props.text || props.title,
|
|
99
|
+
tagline: props.tagline,
|
|
100
|
+
actions: props.actions,
|
|
101
|
+
image: props.image,
|
|
102
|
+
video: props.video,
|
|
103
|
+
background: props.background,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!frontmatter.value.hero || frontmatter.value.hero === false) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// If hero is an object with custom properties
|
|
112
|
+
if (typeof frontmatter.value.hero === "object") {
|
|
113
|
+
return {
|
|
114
|
+
...frontmatter.value.hero,
|
|
115
|
+
text:
|
|
116
|
+
frontmatter.value.hero.text ||
|
|
117
|
+
frontmatter.value.hero.title ||
|
|
118
|
+
frontmatter.value.title,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If hero is true (boolean), provide intelligent layout defaults
|
|
123
|
+
if (frontmatter.value.hero === true) {
|
|
124
|
+
if (frontmatter.value.layout === "team") {
|
|
125
|
+
return {
|
|
126
|
+
text: frontmatter.value.title || "Our Team",
|
|
127
|
+
tagline:
|
|
128
|
+
frontmatter.value.tagline ||
|
|
129
|
+
"Meet the diverse team of geospatial experts, engineers, and visionaries driving EOX forward.",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (frontmatter.value.layout === "blog") {
|
|
133
|
+
const tag = params?.value?.tag;
|
|
134
|
+
const author = params?.value?.author;
|
|
135
|
+
let blogTitle = frontmatter.value.title || "EOX Blog";
|
|
136
|
+
if (tag) {
|
|
137
|
+
blogTitle = `Tag: ${tag}`;
|
|
138
|
+
} else if (author) {
|
|
139
|
+
blogTitle = `Author: ${author}`;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
text: blogTitle,
|
|
143
|
+
tagline:
|
|
144
|
+
frontmatter.value.tagline ||
|
|
145
|
+
"Insights, updates, and deep dives from our geospatial experts.",
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (frontmatter.value.layout === "tags") {
|
|
149
|
+
return {
|
|
150
|
+
text: frontmatter.value.title || "Blog Tags",
|
|
151
|
+
tagline:
|
|
152
|
+
frontmatter.value.tagline ||
|
|
153
|
+
"Explore our blog posts by topics and technology pillars.",
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
text: frontmatter.value.title || "",
|
|
158
|
+
tagline: frontmatter.value.tagline || frontmatter.value.description || "",
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return null;
|
|
163
|
+
});
|
|
76
164
|
</script>
|
|
77
165
|
|
|
78
166
|
<style scoped>
|