@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
@@ -0,0 +1,115 @@
1
+ <template>
2
+ <div class="section-header-group">
3
+ <!-- Section Tag Badge -->
4
+ <div v-if="tag || icon" class="section-badge-tag">
5
+ <i v-if="icon" :class="['mdi', icon, 'section-badge-icon']"></i>
6
+ <span class="section-badge-text">{{ tag }}</span>
7
+ </div>
8
+
9
+ <!-- Display Title -->
10
+ <h2 class="section-display-title">
11
+ <slot name="title">{{ title }}</slot>
12
+ </h2>
13
+
14
+ <!-- Subtitle / Body text -->
15
+ <p v-if="subtitle || $slots.default" class="section-subtitle">
16
+ <slot>{{ subtitle }}</slot>
17
+ </p>
18
+ </div>
19
+ </template>
20
+
21
+ <script setup>
22
+ defineProps({
23
+ tag: {
24
+ type: String,
25
+ required: false,
26
+ default: "",
27
+ },
28
+ icon: {
29
+ type: String,
30
+ required: false,
31
+ default: "",
32
+ },
33
+ title: {
34
+ type: String,
35
+ required: false,
36
+ default: "",
37
+ },
38
+ subtitle: {
39
+ type: String,
40
+ required: false,
41
+ default: "",
42
+ },
43
+ });
44
+ </script>
45
+
46
+ <style scoped>
47
+ .section-header-group {
48
+ display: flex;
49
+ flex-direction: column;
50
+ align-items: flex-start;
51
+ padding: 0 !important;
52
+ margin: 0 0 40px 0 !important;
53
+ gap: 16px;
54
+ width: 100%;
55
+ }
56
+
57
+ .section-badge-tag {
58
+ display: flex;
59
+ flex-direction: row;
60
+ align-items: center;
61
+ padding: 0 !important;
62
+ margin: 0 !important;
63
+ gap: 12px;
64
+ height: 24px;
65
+ }
66
+
67
+ .section-badge-icon {
68
+ width: 24px;
69
+ height: 24px;
70
+ font-size: 24px;
71
+ line-height: 1;
72
+ display: inline-flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ margin: 0 !important;
76
+ padding: 0 !important;
77
+ color: var(--primary, #004170);
78
+ }
79
+
80
+ .section-badge-text {
81
+ font-family: "Inter", sans-serif;
82
+ font-style: normal;
83
+ font-weight: 400;
84
+ font-size: 16px;
85
+ line-height: 24px;
86
+ letter-spacing: 0.5px;
87
+ margin: 0 !important;
88
+ padding: 0 !important;
89
+ color: var(--primary, #004170);
90
+ }
91
+
92
+ .section-display-title {
93
+ font-family: "Inter", sans-serif;
94
+ font-style: normal;
95
+ font-weight: 700;
96
+ font-size: 36px !important;
97
+ line-height: 44px;
98
+ letter-spacing: 0.5008px;
99
+ margin: 0 !important;
100
+ padding: 0 !important;
101
+ border-top: none !important;
102
+ }
103
+
104
+ .section-subtitle {
105
+ font-family: "Inter", sans-serif;
106
+ font-style: normal;
107
+ font-weight: 400;
108
+ font-size: 16px;
109
+ line-height: 24px;
110
+ letter-spacing: 0.5px;
111
+ max-width: 620px;
112
+ margin: 0 !important;
113
+ padding: 0 !important;
114
+ }
115
+ </style>
@@ -0,0 +1,27 @@
1
+ import SingleTweet from "./SingleTweet.vue";
2
+
3
+ describe("<SingleTweet />", () => {
4
+ it("renders consent fallback if cookies are not allowed", () => {
5
+ cy.mount(SingleTweet, {
6
+ props: {
7
+ id: "12345",
8
+ mockConsent: false,
9
+ },
10
+ });
11
+
12
+ cy.contains("Cookie Consent Required").should("be.visible");
13
+ cy.get("blockquote").should("not.exist");
14
+ });
15
+
16
+ it("renders tweet blockquote if cookies are allowed", () => {
17
+ cy.mount(SingleTweet, {
18
+ props: {
19
+ id: "12345",
20
+ mockConsent: true,
21
+ },
22
+ });
23
+
24
+ cy.get("blockquote.twitter-tweet").should("exist");
25
+ cy.contains("Cookie Consent Required").should("not.exist");
26
+ });
27
+ });
@@ -0,0 +1,155 @@
1
+ <template>
2
+ <div class="single-tweet-container">
3
+ <div class="tweet-card">
4
+ <!-- Embed Mode (Cookies accepted or SSR) -->
5
+ <div v-if="cookiesAllowed" class="tweet-embed-wrapper">
6
+ <blockquote class="twitter-tweet" data-theme="light">
7
+ <a :href="`https://twitter.com/x/status/${id}`"></a>
8
+ </blockquote>
9
+ </div>
10
+
11
+ <!-- No Consent Fallback -->
12
+ <div v-else class="fallback-card">
13
+ <svg class="fallback-icon" viewBox="0 0 24 24" fill="currentColor">
14
+ <path
15
+ d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"
16
+ />
17
+ </svg>
18
+ <div class="fallback-text">
19
+ <p class="fallback-title">Cookie Consent Required</p>
20
+ <p class="fallback-desc">
21
+ Please accept optional cookies to load the interactive Tweet. You
22
+ can review your preferences on the
23
+ <a href="/cookie-settings" class="cookie-link">Cookie Settings</a>
24
+ page.
25
+ </p>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { ref, onMounted, watch } from "vue";
34
+
35
+ const props = defineProps({
36
+ id: {
37
+ type: String,
38
+ required: true,
39
+ },
40
+ mockConsent: {
41
+ type: Boolean,
42
+ default: null,
43
+ },
44
+ });
45
+
46
+ const cookiesAllowed = ref(false);
47
+
48
+ const checkConsent = () => {
49
+ if (props.mockConsent !== null) {
50
+ cookiesAllowed.value = props.mockConsent;
51
+ return;
52
+ }
53
+ if (typeof document !== "undefined") {
54
+ // If no mtm cookie banner is present or user has consented
55
+ cookiesAllowed.value =
56
+ document.cookie.includes("mtm_cookie_consent") ||
57
+ !document.querySelector(".cookie-banner");
58
+ }
59
+ };
60
+
61
+ onMounted(() => {
62
+ checkConsent();
63
+
64
+ // Dynamically load Twitter widgets script if allowed
65
+ watch(
66
+ cookiesAllowed,
67
+ (allowed) => {
68
+ if (allowed && typeof window !== "undefined") {
69
+ if (window.twttr) {
70
+ window.twttr.widgets.load();
71
+ } else {
72
+ const script = document.createElement("script");
73
+ script.src = "https://platform.twitter.com/widgets.js";
74
+ script.async = true;
75
+ script.charset = "utf-8";
76
+ document.head.appendChild(script);
77
+ }
78
+ }
79
+ },
80
+ { immediate: true },
81
+ );
82
+ });
83
+ </script>
84
+
85
+ <style scoped>
86
+ .single-tweet-container {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: center;
90
+ margin: 1.5rem 0;
91
+ width: 100%;
92
+ }
93
+
94
+ .tweet-card {
95
+ width: 100%;
96
+ max-width: 500px;
97
+ min-height: 150px;
98
+ display: flex;
99
+ flex-direction: column;
100
+ justify-content: center;
101
+ }
102
+
103
+ .tweet-embed-wrapper {
104
+ width: 100%;
105
+ display: flex;
106
+ justify-content: center;
107
+ }
108
+
109
+ .fallback-card {
110
+ display: flex;
111
+ flex-direction: column;
112
+ align-items: center;
113
+ justify-content: center;
114
+ padding: 2rem;
115
+ background-color: #f1f5f9;
116
+ border: 1px solid #e2e8f0;
117
+ border-radius: 8px;
118
+ text-align: center;
119
+ color: #475569;
120
+ gap: 1rem;
121
+ }
122
+
123
+ .fallback-icon {
124
+ width: 40px;
125
+ height: 40px;
126
+ color: #94a3b8;
127
+ }
128
+
129
+ .fallback-text {
130
+ font-size: 0.875rem;
131
+ }
132
+
133
+ .fallback-title {
134
+ font-weight: 600;
135
+ color: #1e293b;
136
+ margin: 0 0 0.25rem 0;
137
+ }
138
+
139
+ .fallback-desc {
140
+ color: #64748b;
141
+ font-size: 0.75rem;
142
+ line-height: 1.5;
143
+ margin: 0;
144
+ }
145
+
146
+ .cookie-link {
147
+ color: var(--primary, #004170);
148
+ font-weight: 500;
149
+ text-decoration: none;
150
+ }
151
+
152
+ .cookie-link:hover {
153
+ text-decoration: underline;
154
+ }
155
+ </style>
@@ -0,0 +1,293 @@
1
+ <template>
2
+ <div class="tags-page">
3
+ <header class="tags-header">
4
+ <h1 class="tags-title">
5
+ Blog Tags
6
+ </h1>
7
+ <p class="tags-subtitle">
8
+ Explore our blog posts by topics and technology pillars.
9
+ </p>
10
+ </header>
11
+
12
+ <!-- Unified flat list of all tags -->
13
+ <div class="tags-container">
14
+ <div class="tags-flex">
15
+ <a
16
+ v-for="tag in allTagsList"
17
+ :key="tag.name"
18
+ :href="`/tag/${tag.name}/`"
19
+ :style="getTagStyle(tag.name)"
20
+ class="tag-item"
21
+ >
22
+ <span>{{ tag.name }}</span>
23
+ <span class="tag-count">{{ tag.count }}</span>
24
+ </a>
25
+ </div>
26
+ </div>
27
+
28
+ <!-- Tech pillars legend overview footer -->
29
+ <footer class="tags-footer">
30
+ <h3 class="legend-title">Technology Pillars:</h3>
31
+ <ul class="legend-grid">
32
+ <li class="legend-item">
33
+ <span
34
+ class="legend-dot"
35
+ style="background-color: rgb(255, 94, 94)"
36
+ ></span>
37
+ <span>Managed cloud workspace</span>
38
+ </li>
39
+ <li class="legend-item">
40
+ <span
41
+ class="legend-dot"
42
+ style="background-color: rgb(0, 177, 176)"
43
+ ></span>
44
+ <span>Data management</span>
45
+ </li>
46
+ <li class="legend-item">
47
+ <span
48
+ class="legend-dot"
49
+ style="background-color: rgb(219, 173, 66)"
50
+ ></span>
51
+ <span>Data visualization</span>
52
+ </li>
53
+ <li class="legend-item">
54
+ <span
55
+ class="legend-dot"
56
+ style="background-color: rgb(218, 71, 228)"
57
+ ></span>
58
+ <span>Data products</span>
59
+ </li>
60
+ </ul>
61
+ <p class="legend-note">
62
+ For more detailed information on our technology solutions, visit the
63
+ <a href="/software-products" class="legend-link">Solutions & Services</a>
64
+ page.
65
+ </p>
66
+ </footer>
67
+ </div>
68
+ </template>
69
+
70
+ <script setup>
71
+ import { computed } from "vue";
72
+ import { data as posts } from "../posts.data.js";
73
+ import { getTagColor, getTagStyle } from "../tagUtils.js";
74
+
75
+ const allTagsList = computed(() => {
76
+ // Aggregate all tag occurrences
77
+ const tagCounts = {};
78
+ posts.forEach((p) => {
79
+ if (Array.isArray(p.tags)) {
80
+ p.tags.forEach((t) => {
81
+ const clean = t.trim().toLowerCase();
82
+ if (clean) {
83
+ tagCounts[clean] = (tagCounts[clean] || 0) + 1;
84
+ }
85
+ });
86
+ }
87
+ });
88
+
89
+ // Convert to sorted list
90
+ return Object.entries(tagCounts)
91
+ .map(([name, count]) => ({
92
+ name,
93
+ count,
94
+ color: getTagColor(name),
95
+ }))
96
+ .sort((a, b) => a.name.localeCompare(b.name));
97
+ });
98
+ </script>
99
+
100
+ <style scoped>
101
+ .tags-page {
102
+ width: 100%;
103
+ max-width: 1200px;
104
+ margin-left: auto;
105
+ margin-right: auto;
106
+ padding-left: 1rem;
107
+ padding-right: 1rem;
108
+ padding-top: calc(var(--vp-nav-height) + 24px);
109
+ padding-bottom: 48px;
110
+ box-sizing: border-box;
111
+ }
112
+
113
+ @media (min-width: 640px) {
114
+ .tags-page {
115
+ padding-left: 1.5rem;
116
+ padding-right: 1.5rem;
117
+ padding-top: calc(var(--vp-nav-height) + 40px);
118
+ padding-bottom: 64px;
119
+ }
120
+ }
121
+
122
+ @media (min-width: 960px) {
123
+ .tags-page {
124
+ padding-left: 2rem;
125
+ padding-right: 2rem;
126
+ padding-top: calc(var(--vp-nav-height) + 58px);
127
+ padding-bottom: 96px;
128
+ }
129
+ }
130
+
131
+ /* Page Header */
132
+ .tags-header {
133
+ margin-bottom: 2rem;
134
+ border-bottom: 1px solid #f1f5f9; /* slate-100 */
135
+ padding-bottom: 1.5rem;
136
+ text-align: center;
137
+ max-width: 42rem; /* 2xl equivalent */
138
+ margin-left: auto;
139
+ margin-right: auto;
140
+ }
141
+
142
+ .tags-title {
143
+ font-family: "Inter", sans-serif;
144
+ font-style: normal;
145
+ font-weight: 700;
146
+ font-size: 28px;
147
+ line-height: 36px;
148
+ color: #020617; /* slate-950 */
149
+ margin-top: 0;
150
+ margin-bottom: 0.5rem;
151
+ letter-spacing: 0.5px;
152
+ }
153
+
154
+ @media (min-width: 768px) {
155
+ .tags-title {
156
+ font-size: 32px;
157
+ line-height: 40px;
158
+ }
159
+ }
160
+
161
+ .tags-subtitle {
162
+ font-family: "Inter", sans-serif;
163
+ font-style: normal;
164
+ font-weight: 400;
165
+ font-size: 16px;
166
+ line-height: 24px;
167
+ color: #64748b; /* slate-500 */
168
+ max-width: 620px;
169
+ margin-left: auto;
170
+ margin-right: auto;
171
+ margin-top: 0;
172
+ margin-bottom: 0;
173
+ padding: 0;
174
+ letter-spacing: 0.5px;
175
+ }
176
+
177
+ /* Tag Collection Container */
178
+ .tags-container {
179
+ background-color: var(--surface-container-low);
180
+ padding: 2.5rem 2rem;
181
+ border-radius: 1rem;
182
+ border: 1px solid rgba(226, 232, 240, 0.5); /* border-slate-100/50 */
183
+ box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05); /* shadow-sm */
184
+ margin-bottom: 3rem;
185
+ }
186
+
187
+ .tags-flex {
188
+ display: flex;
189
+ flex-wrap: wrap;
190
+ gap: 0.75rem;
191
+ justify-content: center;
192
+ }
193
+
194
+ /* Tag Item Style */
195
+ .tag-item {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 0.5rem;
199
+ font-size: 0.875rem; /* text-sm */
200
+ font-weight: 600;
201
+ padding: 0.375rem 0.875rem;
202
+ border-radius: 4px; /* small rounding to distinguish from full-rounded buttons */
203
+ text-decoration: none !important;
204
+ line-height: 1.2;
205
+ transition: opacity 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
206
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
207
+ }
208
+
209
+ .tag-item:hover {
210
+ opacity: 0.85;
211
+ transform: translateY(-1px);
212
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -2px rgba(0, 0, 0, 0.05);
213
+ }
214
+
215
+ .tag-count {
216
+ font-size: 0.75rem;
217
+ padding: 0.125rem 0.375rem;
218
+ background-color: rgba(255, 255, 255, 0.4);
219
+ color: inherit;
220
+ border-radius: 9999px;
221
+ font-weight: 700;
222
+ }
223
+
224
+ /* Legend Overview Footer */
225
+ .tags-footer {
226
+ background-color: var(--surface-container-low);
227
+ padding: 2rem;
228
+ border-radius: 1rem;
229
+ border: 1px solid rgba(226, 232, 240, 0.5);
230
+ max-width: 48rem; /* 3xl equivalent */
231
+ margin-left: auto;
232
+ margin-right: auto;
233
+ margin-top: 4rem;
234
+ }
235
+
236
+ .legend-title {
237
+ font-family: "Inter", sans-serif;
238
+ font-size: 1.125rem; /* text-lg */
239
+ font-weight: 700;
240
+ color: #020617; /* slate-950 */
241
+ margin-top: 0;
242
+ margin-bottom: 1rem;
243
+ }
244
+
245
+ .legend-grid {
246
+ display: grid;
247
+ grid-template-columns: 1fr;
248
+ gap: 1rem;
249
+ padding-left: 0;
250
+ list-style: none;
251
+ margin: 0;
252
+ }
253
+
254
+ @media (min-width: 640px) {
255
+ .legend-grid {
256
+ grid-template-columns: repeat(2, minmax(0, 1fr));
257
+ }
258
+ }
259
+
260
+ .legend-item {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 0.625rem;
264
+ font-size: 0.875rem; /* text-sm */
265
+ color: #475569; /* slate-600 */
266
+ }
267
+
268
+ .legend-dot {
269
+ width: 0.875rem;
270
+ height: 0.875rem;
271
+ border-radius: 9999px;
272
+ flex-shrink: 0;
273
+ }
274
+
275
+ .legend-note {
276
+ font-size: 0.875rem;
277
+ color: #64748b; /* slate-500 */
278
+ margin-top: 1.5rem;
279
+ padding-top: 1rem;
280
+ border-top: 1px solid rgba(226, 232, 240, 0.6);
281
+ line-height: 1.5;
282
+ }
283
+
284
+ .legend-link {
285
+ color: var(--primary, #004170);
286
+ font-weight: 600;
287
+ text-decoration: none;
288
+ }
289
+
290
+ .legend-link:hover {
291
+ text-decoration: underline;
292
+ }
293
+ </style>