@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,450 @@
1
+ <template>
2
+ <div class="blog-post-page VPDoc max-w-[1000px] mx-auto px-6 md:px-8">
3
+ <div class="blog-post-layout">
4
+ <!-- Main Article Column -->
5
+ <div class="blog-content-col">
6
+ <!-- Cover Image -->
7
+ <div
8
+ v-if="coverImage"
9
+ class="mb-8 rounded-2xl overflow-hidden shadow-sm max-h-[550px] bg-slate-50 border border-slate-100"
10
+ >
11
+ <img
12
+ :src="getCoverImageUrl(coverImage)"
13
+ :alt="frontmatter.title"
14
+ class="w-full h-full object-cover max-h-[550px]"
15
+ />
16
+ </div>
17
+
18
+ <!-- Title -->
19
+ <h1 class="blog-post-title">
20
+ {{ frontmatter.title }}
21
+ </h1>
22
+
23
+ <!-- Metadata Row (Date & Authors) and Tags Underneath -->
24
+ <div class="mb-8 border-b border-slate-100 pb-6">
25
+ <div class="blog-metadata">
26
+ <!-- Date Span -->
27
+ <span v-if="frontmatter.date" class="metadata-item">
28
+ <i class="mdi mdi-clock-outline metadata-icon"></i>
29
+ <time :datetime="frontmatter.date" class="metadata-text">
30
+ {{ formatDate(frontmatter.date) }}
31
+ </time>
32
+ </span>
33
+
34
+ <!-- Author Span -->
35
+ <span v-if="authors && authors.length" class="metadata-item">
36
+ <i class="mdi mdi-account-outline metadata-icon"></i>
37
+ <span class="metadata-text label-text">By</span>
38
+ <span
39
+ v-for="(authorId, idx) in authors"
40
+ :key="authorId"
41
+ class="author-item"
42
+ >
43
+ <a
44
+ :href="`/author/${authorId}/`"
45
+ class="author-link-active"
46
+ >
47
+ {{ getAuthorName(authorId) }}
48
+ </a>
49
+ <span v-if="idx < authors.length - 1" class="author-separator">,</span>
50
+ </span>
51
+ </span>
52
+ </div>
53
+
54
+ <!-- Tags right underneath -->
55
+ <div
56
+ v-if="frontmatter.tags && frontmatter.tags.length"
57
+ class="post-tags-container"
58
+ >
59
+ <a
60
+ v-for="tag in frontmatter.tags"
61
+ :key="tag"
62
+ :href="`/tag/${tag}/`"
63
+ :style="getTagStyle(tag)"
64
+ class="post-tag"
65
+ >
66
+ {{ tag }}
67
+ </a>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Main Content -->
72
+ <div class="vp-doc prose prose-slate max-w-none">
73
+ <Content />
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Sidebar Column -->
78
+ <div class="blog-sidebar-col">
79
+ <!-- Table of Contents (On This Page) -->
80
+ <div
81
+ v-if="headers && headers.length"
82
+ class="toc-container"
83
+ >
84
+ <VPDocAsideOutline />
85
+ </div>
86
+
87
+ <!-- Hot Tags -->
88
+ <div class="sidebar-widget">
89
+ <h3 class="sidebar-title">
90
+ Hot tags
91
+ </h3>
92
+ <div class="sidebar-tags-container">
93
+ <a
94
+ v-for="tag in calculatedHotTags"
95
+ :key="tag.name"
96
+ :href="`/tag/${tag.name}/`"
97
+ :style="getTagStyle(tag.name)"
98
+ class="sidebar-tag"
99
+ >
100
+ {{ tag.name }} ({{ tag.count }})
101
+ </a>
102
+ </div>
103
+ </div>
104
+
105
+ <!-- Recent Posts -->
106
+ <div class="sidebar-widget">
107
+ <h3 class="sidebar-title">
108
+ Recent posts
109
+ </h3>
110
+ <div class="space-y-4">
111
+ <div
112
+ v-for="post in calculatedRecentPosts"
113
+ :key="post.url"
114
+ class="flex flex-col gap-1 border-b border-slate-100 last:border-none pb-3 last:pb-0"
115
+ >
116
+ <a
117
+ :href="post.url"
118
+ class="text-sm font-semibold text-slate-900 hover:text-primary leading-snug transition-colors line-clamp-2"
119
+ >
120
+ {{ post.title }}
121
+ </a>
122
+ <time :datetime="post.date" class="text-xs text-slate-500">
123
+ {{ formatDate(post.date) }}
124
+ </time>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ </template>
132
+
133
+ <script setup>
134
+ import { useData } from "vitepress";
135
+ import { computed } from "vue";
136
+ import { data as posts } from "../posts.data.js";
137
+ import { data as authorsList } from "../authors.data.js";
138
+ import { getTagColor, getTagStyle } from "../tagUtils.js";
139
+ import VPDocAsideOutline from "vitepress/dist/client/theme-default/components/VPDocAsideOutline.vue";
140
+
141
+ const { frontmatter, page } = useData();
142
+
143
+ // Set default outline level to deep for blog posts so h3 headers are resolved
144
+ if (frontmatter.value && frontmatter.value.outline === undefined) {
145
+ frontmatter.value.outline = "deep";
146
+ }
147
+
148
+ // Extract headers
149
+ const headers = computed(() => {
150
+ return page.value.headers || [];
151
+ });
152
+
153
+ // Form cover image URL correctly (prefixing filename with /images/)
154
+ function getCoverImageUrl(coverImage) {
155
+ if (!coverImage) return "";
156
+ if (coverImage.startsWith("/") || coverImage.startsWith("http")) {
157
+ return coverImage;
158
+ }
159
+ return `/images/${coverImage}`;
160
+ }
161
+
162
+ // Format publication date
163
+ function formatDate(dateStr) {
164
+ if (!dateStr) return "";
165
+ const date = new Date(dateStr);
166
+ return date.toLocaleDateString("en-US", {
167
+ year: "numeric",
168
+ month: "long",
169
+ day: "numeric",
170
+ });
171
+ }
172
+
173
+ // Extract authors
174
+ const authors = computed(() => {
175
+ const author = frontmatter.value.author || "";
176
+ return Array.isArray(author) ? author : [author];
177
+ });
178
+
179
+ // Get author full name
180
+ function getAuthorName(authorId) {
181
+ const auth = authorsList.find((a) => a.id === authorId);
182
+ return auth ? auth.name : authorId;
183
+ }
184
+
185
+ // Cover image path
186
+ const coverImage = computed(() => {
187
+ return frontmatter.value.coverImage || frontmatter.value.image || "";
188
+ });
189
+
190
+ // Dynamic Recent Posts (excluding current page)
191
+ const calculatedRecentPosts = computed(() => {
192
+ return posts.filter((p) => p.url !== page.value.url).slice(0, 5);
193
+ });
194
+
195
+ // Dynamic Hot Tags
196
+ const calculatedHotTags = computed(() => {
197
+ const tagCounts = {};
198
+ posts.forEach((p) => {
199
+ if (Array.isArray(p.tags)) {
200
+ p.tags.forEach((t) => {
201
+ const clean = t.trim();
202
+ if (clean) {
203
+ tagCounts[clean] = (tagCounts[clean] || 0) + 1;
204
+ }
205
+ });
206
+ }
207
+ });
208
+ return Object.entries(tagCounts)
209
+ .map(([name, count]) => ({
210
+ name,
211
+ count,
212
+ color: getTagColor(name),
213
+ }))
214
+ .sort((a, b) => b.count - a.count)
215
+ .slice(0, 10);
216
+ });
217
+ </script>
218
+
219
+ <style scoped>
220
+ .blog-post-page {
221
+ width: 100% !important;
222
+ max-width: 1000px !important;
223
+ margin-left: auto !important;
224
+ margin-right: auto !important;
225
+ padding-left: 1.5rem !important;
226
+ padding-right: 1.5rem !important;
227
+ box-sizing: border-box !important;
228
+ padding-top: calc(var(--vp-nav-height) + 0.5rem) !important;
229
+ padding-bottom: 2rem !important;
230
+ }
231
+
232
+ @media (min-width: 768px) {
233
+ .blog-post-page {
234
+ padding-left: 2rem !important;
235
+ padding-right: 2rem !important;
236
+ padding-top: calc(var(--vp-nav-height) + 1.5rem) !important;
237
+ padding-bottom: 4rem !important;
238
+ }
239
+ }
240
+
241
+ .blog-post-layout {
242
+ display: grid;
243
+ grid-template-columns: 1fr;
244
+ gap: 2.5rem;
245
+ }
246
+
247
+ .blog-content-col {
248
+ width: 100%;
249
+ min-width: 0;
250
+ /* Prevent Beercss style inheritance */
251
+ background: transparent !important;
252
+ box-shadow: none !important;
253
+ border: none !important;
254
+ padding: 0 !important;
255
+ }
256
+
257
+ .blog-sidebar-col {
258
+ width: 100%;
259
+ display: flex;
260
+ flex-direction: column;
261
+ gap: 2.5rem;
262
+ }
263
+
264
+ @media (min-width: 1024px) {
265
+ .blog-post-layout {
266
+ grid-template-columns: minmax(0, 1fr) 280px;
267
+ gap: 4rem;
268
+ align-items: start;
269
+ }
270
+ .blog-sidebar-col {
271
+ position: sticky !important;
272
+ align-self: start !important;
273
+ top: calc(var(--vp-nav-height) + 2rem) !important;
274
+ max-height: calc(100vh - var(--vp-nav-height) - 4rem);
275
+ overflow-y: auto;
276
+ scrollbar-width: none; /* Hide scrollbar for cleaner look */
277
+ }
278
+ .blog-sidebar-col::-webkit-scrollbar {
279
+ display: none;
280
+ }
281
+ }
282
+
283
+ .blog-post-title {
284
+ font-family: "Inter", sans-serif;
285
+ font-style: normal;
286
+ font-weight: 700;
287
+ font-size: 28px;
288
+ line-height: 36px;
289
+ letter-spacing: 0.5px;
290
+ color: #020617; /* slate-950 */
291
+ margin-top: 0;
292
+ margin-bottom: 1.5rem;
293
+ }
294
+
295
+ @media (min-width: 768px) {
296
+ .blog-post-title {
297
+ font-size: 36px;
298
+ line-height: 44px;
299
+ }
300
+ }
301
+
302
+ .blog-metadata {
303
+ display: flex;
304
+ flex-wrap: wrap;
305
+ align-items: center;
306
+ gap: 1.5rem;
307
+ margin-bottom: 1rem;
308
+ }
309
+
310
+ .metadata-item {
311
+ display: inline-flex;
312
+ align-items: center;
313
+ gap: 0.5rem;
314
+ }
315
+
316
+ .metadata-icon {
317
+ font-size: 1.1rem !important;
318
+ color: #64748b;
319
+ display: inline-flex;
320
+ align-items: center;
321
+ justify-content: center;
322
+ height: 100%;
323
+ transform: translateY(0.5px); /* Micro-adjustment for perfect vertical alignment */
324
+ }
325
+
326
+ .metadata-text {
327
+ font-size: 0.875rem;
328
+ color: #64748b;
329
+ line-height: 1.2;
330
+ }
331
+
332
+ .label-text {
333
+ margin-right: -0.125rem;
334
+ }
335
+
336
+ .author-item {
337
+ display: inline-flex;
338
+ align-items: center;
339
+ }
340
+
341
+ .author-link-active {
342
+ font-size: 0.875rem;
343
+ font-weight: 600;
344
+ color: var(--vp-c-brand, #004170) !important;
345
+ text-decoration: none;
346
+ transition: color 0.15s ease, text-decoration 0.15s ease;
347
+ line-height: 1.2;
348
+ }
349
+
350
+ .author-link-active:hover {
351
+ color: var(--vp-c-brand-next, #002d50) !important;
352
+ text-decoration: underline !important;
353
+ }
354
+
355
+ .author-separator {
356
+ color: #64748b;
357
+ margin-right: 0.25rem;
358
+ font-size: 0.875rem;
359
+ }
360
+
361
+ .sidebar-widget {
362
+ display: flex;
363
+ flex-direction: column;
364
+ gap: 0.75rem;
365
+ }
366
+
367
+ .sidebar-title {
368
+ font-size: 0.75rem;
369
+ font-weight: 600;
370
+ text-transform: uppercase;
371
+ letter-spacing: 0.05em;
372
+ color: #64748b; /* slate-500 */
373
+ border-bottom: 1px solid #f1f5f9; /* slate-100 */
374
+ padding-bottom: 0.5rem;
375
+ margin: 0;
376
+ }
377
+
378
+ .sidebar-tags-container {
379
+ display: flex !important;
380
+ flex-wrap: wrap !important;
381
+ gap: 0.5rem !important;
382
+ margin-top: 0.25rem !important;
383
+ }
384
+
385
+ .sidebar-tag {
386
+ display: inline-flex !important;
387
+ align-items: center !important;
388
+ font-size: 0.75rem !important;
389
+ font-weight: 600 !important;
390
+ padding: 0.25rem 0.75rem !important;
391
+ border-radius: 4px !important;
392
+ text-decoration: none !important;
393
+ line-height: 1.2 !important;
394
+ transition: opacity 0.2s ease, transform 0.1s ease !important;
395
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
396
+ }
397
+
398
+ .sidebar-tag:hover {
399
+ opacity: 0.8 !important;
400
+ transform: translateY(-1px) !important;
401
+ }
402
+
403
+ .post-tags-container {
404
+ display: flex !important;
405
+ flex-wrap: wrap !important;
406
+ gap: 0.5rem !important;
407
+ margin-top: 0.75rem !important;
408
+ }
409
+
410
+ .post-tag {
411
+ display: inline-flex !important;
412
+ align-items: center !important;
413
+ font-size: 0.75rem !important;
414
+ font-weight: 600 !important;
415
+ padding: 0.25rem 0.75rem !important;
416
+ border-radius: 4px !important;
417
+ text-decoration: none !important;
418
+ line-height: 1.2 !important;
419
+ transition: opacity 0.2s ease, transform 0.1s ease !important;
420
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important;
421
+ }
422
+
423
+ .post-tag:hover {
424
+ opacity: 0.8 !important;
425
+ transform: translateY(-1px) !important;
426
+ }
427
+
428
+ /* Force VPDocAsideOutline to be visible on all screen sizes in our custom layout */
429
+ :deep(.VPDocAsideOutline) {
430
+ display: block !important;
431
+ }
432
+
433
+ :deep(.VPDocAsideOutline.has-outline) {
434
+ display: block !important;
435
+ }
436
+
437
+ .line-clamp-2 {
438
+ display: -webkit-box;
439
+ -webkit-line-clamp: 2;
440
+ -webkit-box-orient: vertical;
441
+ overflow: hidden;
442
+ }
443
+ </style>
444
+
445
+ <style>
446
+ /* Global override for main tag to ensure position: sticky is never blocked on blog pages */
447
+ main {
448
+ overflow: visible !important;
449
+ }
450
+ </style>