@eox/pages-theme-eox 1.9.0 → 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 (53) hide show
  1. package/.gitlab-ci.yml +4 -1
  2. package/README.md +88 -11
  3. package/cypress/support/mocks/authors.data.js +14 -0
  4. package/cypress/support/mocks/helpers.js +65 -4
  5. package/cypress/support/mocks/vitepress.js +8 -1
  6. package/cypress.config.js +14 -0
  7. package/package.json +3 -1
  8. package/src/Layout.vue +69 -1
  9. package/src/authors.data.js +14 -0
  10. package/src/components/BlogList.vue +655 -0
  11. package/src/components/BlogPost.vue +472 -0
  12. package/src/components/CTASection.vue +18 -4
  13. package/src/components/FeatureCard/Index.vue +462 -31
  14. package/src/components/FeatureCard/Poster.vue +25 -3
  15. package/src/components/FeatureSection.cy.js +16 -0
  16. package/src/components/FeatureSection.vue +3 -8
  17. package/src/components/FeaturesGallery.vue +34 -2
  18. package/src/components/Footer.cy.js +52 -2
  19. package/src/components/Footer.vue +17 -5
  20. package/src/components/HeroSection.cy.js +20 -0
  21. package/src/components/HeroSection.vue +140 -25
  22. package/src/components/ImageCarousel.vue +11 -2
  23. package/src/components/ImageElement.vue +16 -4
  24. package/src/components/ImageGallery.vue +29 -4
  25. package/src/components/LogoSection.vue +26 -3
  26. package/src/components/MobileNavDropdown.vue +13 -3
  27. package/src/components/NavBar.cy.js +15 -0
  28. package/src/components/NavBar.vue +149 -21
  29. package/src/components/NavDropdown.vue +12 -2
  30. package/src/components/OpenPositions.vue +292 -0
  31. package/src/components/SectionHeader.cy.js +54 -0
  32. package/src/components/SectionHeader.vue +115 -0
  33. package/src/components/TagList.vue +311 -0
  34. package/src/components/TeamPage.vue +347 -0
  35. package/src/components/TestimonialsSection.cy.js +76 -0
  36. package/src/components/TestimonialsSection.vue +368 -0
  37. package/src/components/TextSection.cy.js +115 -0
  38. package/src/components/TextSection.vue +396 -0
  39. package/src/helpers.js +182 -37
  40. package/src/helpers.test.js +181 -0
  41. package/src/index.js +16 -0
  42. package/src/pages/author/[author].md +5 -0
  43. package/src/pages/author/[author].paths.js +60 -0
  44. package/src/pages/blog.md +5 -0
  45. package/src/pages/tag/[tag].md +5 -0
  46. package/src/pages/tag/[tag].paths.js +49 -0
  47. package/src/pages/tag.md +5 -0
  48. package/src/pages/team.md +5 -0
  49. package/src/posts.data.js +134 -0
  50. package/src/style.css +389 -8
  51. package/src/tagUtils.js +148 -0
  52. package/src/team.data.js +1 -1
  53. package/src/vitepressConfig.mjs +101 -1
package/.gitlab-ci.yml CHANGED
@@ -10,9 +10,11 @@ cache:
10
10
  - package-lock.json
11
11
  paths:
12
12
  - .npm/
13
+ - .cache/Cypress
13
14
 
14
15
  variables:
15
16
  npm_config_cache: "$CI_PROJECT_DIR/.npm"
17
+ CYPRESS_CACHE_FOLDER: "$CI_PROJECT_DIR/.cache/Cypress"
16
18
 
17
19
  security:
18
20
  stage: security
@@ -33,6 +35,7 @@ test:
33
35
  stage: test
34
36
  script:
35
37
  - npm ci
36
- - npm run test:component
38
+ - npx cypress install
39
+ - npm run test
37
40
  rules:
38
41
  - if: $CI_PIPELINE_SOURCE == 'merge_request_event' && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
package/README.md CHANGED
@@ -145,26 +145,40 @@ The theme provides several built-in components and layout features. Some are dri
145
145
 
146
146
  ### Hero Section
147
147
 
148
- 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.
149
149
 
150
150
  ```yaml
151
151
  ---
152
152
  hero:
153
- title: "Welcome to My Site"
154
- tagline: "Building the future of things"
153
+ text: "Welcome to My Site" # or title: "Welcome to My Site"
154
+ tagline: "Building the future of geospatial engineering"
155
155
  image:
156
156
  src: "/hero-image.png"
157
157
  alt: "Hero Image"
158
158
  actions:
159
159
  - text: "Get Started"
160
160
  link: "/start"
161
- type: "primary"
161
+ theme: "brand" # "brand" (primary surface), "secondary", or "alt" (bordered)
162
162
  - text: "Learn More"
163
163
  link: "/about"
164
- type: "secondary"
164
+ theme: "secondary"
165
165
  ---
166
166
  ```
167
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
+
168
182
  ### Feature Section
169
183
 
170
184
  Use the `<FeatureSection>` component to highlight specific features.
@@ -223,7 +237,7 @@ A flexible pricing table component.
223
237
 
224
238
  #### CTA Section
225
239
 
226
- 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.
227
241
 
228
242
  ```html
229
243
  <CTASection
@@ -239,6 +253,19 @@ A Call-to-Action section typically placed at the bottom of a page to encourage u
239
253
  />
240
254
  ```
241
255
 
256
+ #### Section Header
257
+
258
+ A standardized three-tier header block for page sections, matching EOX design tokens with a tag badge, a large display title, and an optional subtitle description. Supports light/dark context modes automatically.
259
+
260
+ ```html
261
+ <SectionHeader
262
+ tag="Viewing Ready & Basic"
263
+ icon="mdi-star-check-outline"
264
+ title="What our users say about Viewing Products"
265
+ subtitle="Optional paragraph providing context and detail for this section."
266
+ />
267
+ ```
268
+
242
269
  #### Logo Section
243
270
 
244
271
  Displays a grid of client or partner logos. It automatically handles logo sizing.
@@ -321,7 +348,7 @@ A standard content card with an icon or image, title, optional chips, metadata,
321
348
  section-title="All Features"
322
349
  variant="default"
323
350
  columns="3/2/1"
324
- background="surface-container-low"
351
+ background="surface-container-lowest"
325
352
  :cards="[
326
353
  {
327
354
  id: 1,
@@ -559,7 +586,14 @@ feedback: false
559
586
  The theme exports a set of shared utility and helper functions from its helpers module. You can import and use these helpers in your custom VitePress components, checkout pages, or inline script setups:
560
587
 
561
588
  ```javascript
562
- import { formatPrice, trackEvent, t } from "@eox/pages-theme-eox/src/helpers";
589
+ import {
590
+ formatPrice,
591
+ trackEvent,
592
+ trackPageView,
593
+ trackSiteSearch,
594
+ normalizeTrackingUrl,
595
+ t,
596
+ } from "@eox/pages-theme-eox/src/helpers";
563
597
  ```
564
598
 
565
599
  ### Available Helpers
@@ -585,12 +619,12 @@ formatPrice(12000, "en"); // "€ 12,000.-"
585
619
  formatPrice(49.99, "de"); // "€ 49,99"
586
620
  ```
587
621
 
588
- #### `trackEvent(eventDetails)`
622
+ #### `trackEvent(eventDetails, ...rest)`
589
623
 
590
- Sends a custom tracking event to Matomo Analytics.
624
+ Sends a custom tracking event to Matomo Analytics. Automatically normalizes any path strings passed in category, action, or name.
591
625
 
592
626
  - **Parameters**:
593
- - `eventDetails` (`Array<string>`): An array representing the event payload: `[Category, Action, Name?, Value?]`.
627
+ - `eventDetails` (`Array<string|number>|string`): An array `[Category, Action, Name?, Value?]` or positional arguments `(category, action, name, value)`.
594
628
 
595
629
  ##### Example Usage:
596
630
 
@@ -598,8 +632,51 @@ Sends a custom tracking event to Matomo Analytics.
598
632
  import { trackEvent } from "@eox/pages-theme-eox/src/helpers";
599
633
 
600
634
  trackEvent(["CTA", "Click", "Technical Specifications", "Viewing Ready"]);
635
+ trackEvent("indicators", "select_indicator", "N1_NO2");
636
+ ```
637
+
638
+ #### `trackPageView(options)`
639
+
640
+ Tracks a pageview with normalized path (trailing slash stripped, transient parameters removed) and custom document title.
641
+
642
+ - **Parameters**:
643
+ - `options` (`Object|string`): Vitepress router object, `{ path, title }` options object, or custom path string.
644
+
645
+ ##### Example Usage:
646
+
647
+ ```javascript
648
+ import { trackPageView } from "@eox/pages-theme-eox/src/helpers";
649
+
650
+ trackPageView({ path: "/explore/", title: "Explore" });
651
+ // Sends normalized URL "/explore" to Matomo
652
+ ```
653
+
654
+ #### `trackSiteSearch(keyword, category, resultsCount)`
655
+
656
+ Sends search keyword telemetry to Matomo Site Search reports.
657
+
658
+ - **Parameters**:
659
+ - `keyword` (`string`): Searched term.
660
+ - `category` (`string|boolean`): Category (optional, e.g. `'indicators'`, `'stories'`).
661
+ - `resultsCount` (`number`): Number of search results returned.
662
+
663
+ ##### Example Usage:
664
+
665
+ ```javascript
666
+ import { trackSiteSearch } from "@eox/pages-theme-eox/src/helpers";
667
+
668
+ trackSiteSearch("methane", "indicators", 4);
601
669
  ```
602
670
 
671
+ #### `normalizeTrackingUrl(inputUrl, options)`
672
+
673
+ Pure helper that strips trailing slashes (e.g., `/explore/` -> `/explore`), strips volatile coordinate query params (`x`, `y`, `z`), and sorts remaining query parameters.
674
+
675
+ - **Parameters**:
676
+ - `inputUrl` (`string`): URL or path to normalize.
677
+ - `options` (`Object`): Optional configuration (`ignoredParams`, `stripTrailingSlash`).
678
+ - **Returns**: `string` - Normalized URL string.
679
+
603
680
  #### `t(key, i18n)`
604
681
 
605
682
  Retrieves localized translation strings from an i18n mapping object using dot-notation. Falls back to the key itself if no translation is defined.
@@ -0,0 +1,14 @@
1
+ export const data = [
2
+ {
3
+ id: "mock-author-1",
4
+ name: "Mock Author 1",
5
+ url: "/team/mock-author-1",
6
+ guest: false,
7
+ },
8
+ {
9
+ id: "mock-author-2",
10
+ name: "Mock Author 2",
11
+ url: "/team/mock-author-2",
12
+ guest: false,
13
+ },
14
+ ];
@@ -1,7 +1,40 @@
1
1
  // Mock generic helpers
2
2
  // We delegate to a global object to allow spying in tests
3
3
  window.__helpersMock = {
4
- trackEvent: (details) => {},
4
+ trackEvent: (details, ...rest) => {},
5
+ trackPageView: (options) => {},
6
+ trackSiteSearch: (keyword, category, count) => {},
7
+ normalizeTrackingUrl: (url, options) => {
8
+ if (!url || typeof url !== "string") return url;
9
+ const { ignoredParams = ["x", "y", "z"], stripTrailingSlash = true } =
10
+ options || {};
11
+ try {
12
+ const base =
13
+ typeof window !== "undefined" && window.location?.href
14
+ ? window.location.href
15
+ : "https://eox.at";
16
+ const u = new URL(url, base);
17
+ let pathname = u.pathname;
18
+ if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith("/")) {
19
+ pathname = pathname.replace(/\/+$/, "");
20
+ }
21
+ const searchParams = new URLSearchParams(u.search);
22
+ for (const key of ignoredParams) {
23
+ searchParams.delete(key);
24
+ }
25
+ searchParams.sort();
26
+ const search = searchParams.toString();
27
+ const isRelative = !/^https?:\/\//i.test(url);
28
+ if (isRelative) {
29
+ return pathname + (search ? `?${search}` : "");
30
+ }
31
+ u.pathname = pathname;
32
+ u.search = search ? `?${search}` : "";
33
+ return u.toString();
34
+ } catch {
35
+ return url;
36
+ }
37
+ },
5
38
  acceptCookies: (router) => {},
6
39
  declineCookies: (router) => {},
7
40
  enableTracking: (enable, router) => {},
@@ -15,9 +48,31 @@ window.__helpersMock = {
15
48
  }
16
49
  },
17
50
  isActive: (item, path) => {
18
- if (item.link === path) return true;
19
- if (item.items) {
20
- return item.items.some((sub) => window.__helpersMock.isActive(sub, path));
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 + "/");
21
76
  }
22
77
  return false;
23
78
  },
@@ -94,6 +149,12 @@ window.__helpersMock = {
94
149
  };
95
150
 
96
151
  export const trackEvent = (...args) => window.__helpersMock.trackEvent(...args);
152
+ export const trackPageView = (...args) =>
153
+ window.__helpersMock.trackPageView(...args);
154
+ export const trackSiteSearch = (...args) =>
155
+ window.__helpersMock.trackSiteSearch(...args);
156
+ export const normalizeTrackingUrl = (...args) =>
157
+ window.__helpersMock.normalizeTrackingUrl(...args);
97
158
  export const acceptCookies = (...args) =>
98
159
  window.__helpersMock.acceptCookies(...args);
99
160
  export const declineCookies = (...args) =>
@@ -1,4 +1,11 @@
1
- import { reactive, ref } from "vue";
1
+ import { reactive, ref, defineComponent, h } from "vue";
2
+
3
+ export const VPNavBarSearch = defineComponent({
4
+ name: "VPNavBarSearch",
5
+ render() {
6
+ return h("div", { class: "VPNavBarSearch" }, "Search");
7
+ },
8
+ });
2
9
 
3
10
  const site = ref({
4
11
  title: "Test Site",
package/cypress.config.js CHANGED
@@ -10,6 +10,11 @@ export default defineConfig({
10
10
  viteConfig: {
11
11
  resolve: {
12
12
  alias: {
13
+ // Alias vitepress/theme to our mock
14
+ "vitepress/theme": path.resolve(
15
+ __dirname,
16
+ "cypress/support/mocks/vitepress.js",
17
+ ),
13
18
  // Alias vitepress to our mock
14
19
  vitepress: path.resolve(
15
20
  __dirname,
@@ -29,6 +34,15 @@ export default defineConfig({
29
34
  __dirname,
30
35
  "cypress/support/mocks/features.data.js",
31
36
  ),
37
+ // Alias authors.data.js to our mock
38
+ "../authors.data.js": path.resolve(
39
+ __dirname,
40
+ "cypress/support/mocks/authors.data.js",
41
+ ),
42
+ "../../authors.data.js": path.resolve(
43
+ __dirname,
44
+ "cypress/support/mocks/authors.data.js",
45
+ ),
32
46
  },
33
47
  },
34
48
  plugins: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eox/pages-theme-eox",
3
- "version": "1.9.0",
3
+ "version": "2.0.0-alpha.10",
4
4
  "type": "module",
5
5
  "description": "Vitepress Theme with EOX branding",
6
6
  "main": "src/index.js",
@@ -12,6 +12,8 @@
12
12
  "scripts": {
13
13
  "format": "npx prettier --write .",
14
14
  "format:check": "npx prettier --check .",
15
+ "test": "npm run test:unit && npm run test:component",
16
+ "test:unit": "node --test src/helpers.test.js",
15
17
  "test:component": "cypress run --component",
16
18
  "release": "release-it"
17
19
  },
package/src/Layout.vue CHANGED
@@ -1,5 +1,64 @@
1
1
  <template>
2
- <Layout :class="`layout-${frontmatter.layout}`">
2
+ <div
3
+ v-if="frontmatter.layout === 'blog'"
4
+ class="Layout layout-blog min-h-screen flex flex-col"
5
+ >
6
+ <NavBar />
7
+ <NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
8
+ <HeroSection />
9
+ <main class="flex-grow">
10
+ <BlogList />
11
+ <CTASection
12
+ v-if="frontmatter.cta !== false"
13
+ title="Interested in applying EO data to your use case?"
14
+ tagline="Let's build cloud-native systems, interactive maps, or customized Sentinel data layers together. Reach out to discuss your project requirements."
15
+ primaryButton="Contact us"
16
+ primaryLink="/contact"
17
+ dark
18
+ />
19
+ </main>
20
+ <Footer v-if="frontmatter.footer !== false" />
21
+ <CookieBanner v-if="theme.theme.brandConfig?.analytics"></CookieBanner>
22
+ </div>
23
+ <div
24
+ v-else-if="frontmatter.layout === 'tags'"
25
+ class="Layout layout-tags min-h-screen flex flex-col"
26
+ >
27
+ <NavBar />
28
+ <NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
29
+ <HeroSection />
30
+ <main class="flex-grow">
31
+ <TagList />
32
+ </main>
33
+ <Footer v-if="frontmatter.footer !== false" />
34
+ <CookieBanner v-if="theme.theme.brandConfig?.analytics"></CookieBanner>
35
+ </div>
36
+ <div
37
+ v-else-if="frontmatter.layout === 'team'"
38
+ class="Layout layout-team min-h-screen flex flex-col"
39
+ >
40
+ <NavBar />
41
+ <NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
42
+ <HeroSection />
43
+ <main class="flex-grow">
44
+ <TeamPage />
45
+ </main>
46
+ <Footer v-if="frontmatter.footer !== false" />
47
+ <CookieBanner v-if="theme.theme.brandConfig?.analytics"></CookieBanner>
48
+ </div>
49
+ <div
50
+ v-else-if="frontmatter.layout === 'post' || isBlogPost"
51
+ class="layout-post min-h-screen flex flex-col"
52
+ >
53
+ <NavBar />
54
+ <NewsBanner v-if="theme.newsBanner" :content="theme.newsBanner" />
55
+ <main class="flex-grow">
56
+ <BlogPost />
57
+ </main>
58
+ <Footer v-if="frontmatter.footer !== false" />
59
+ <CookieBanner v-if="theme.theme.brandConfig?.analytics"></CookieBanner>
60
+ </div>
61
+ <Layout v-else :class="`layout-${frontmatter.layout}`">
3
62
  <template #layout-top>
4
63
  <slot name="layout-top"></slot>
5
64
  <NavBar />
@@ -33,11 +92,20 @@
33
92
  </template>
34
93
 
35
94
  <script setup>
95
+ import { computed } from "vue";
36
96
  import DefaultTheme from "vitepress/theme";
37
97
  import { useData, useRouter } from "vitepress";
38
98
  const { Layout } = DefaultTheme;
39
99
  const { frontmatter, page, theme } = useData();
40
100
  const router = useRouter();
101
+
102
+ const isBlogPost = computed(() => {
103
+ return (
104
+ frontmatter.value.layout === "post" ||
105
+ page.value.relativePath.startsWith("_posts/") ||
106
+ /^\d{4}\/\d{2}\//.test(page.value.relativePath)
107
+ );
108
+ });
41
109
  </script>
42
110
 
43
111
  <style>
@@ -0,0 +1,14 @@
1
+ import { createContentLoader } from "vitepress";
2
+
3
+ export default createContentLoader("_team/*.md", {
4
+ transform(rawData) {
5
+ return rawData.map((page) => {
6
+ return {
7
+ id: page.frontmatter.id || "",
8
+ name: page.frontmatter.title || "",
9
+ url: page.url,
10
+ guest: page.frontmatter.guest || false,
11
+ };
12
+ });
13
+ },
14
+ });