@eox/pages-theme-eox 1.9.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.
- package/README.md +13 -0
- package/cypress/support/mocks/vitepress.js +8 -1
- package/cypress.config.js +5 -0
- package/package.json +1 -1
- package/src/Layout.vue +58 -1
- package/src/authors.data.js +14 -0
- package/src/components/BlogList.vue +636 -0
- package/src/components/BlogPost.vue +450 -0
- package/src/components/FeatureCard/Index.vue +370 -26
- package/src/components/HeroSection.vue +1 -1
- package/src/components/SectionHeader.cy.js +54 -0
- package/src/components/SectionHeader.vue +115 -0
- package/src/components/TagList.vue +293 -0
- package/src/components/TeamPage.vue +333 -0
- package/src/components/TestimonialsSection.cy.js +79 -0
- package/src/components/TestimonialsSection.vue +265 -0
- package/src/index.js +12 -0
- package/src/pages/author/[author].md +4 -0
- package/src/pages/author/[author].paths.js +60 -0
- package/src/pages/blog.md +4 -0
- package/src/pages/tag/[tag].md +4 -0
- package/src/pages/tag/[tag].paths.js +49 -0
- package/src/pages/tag.md +4 -0
- package/src/pages/team.md +4 -0
- package/src/posts.data.js +96 -0
- package/src/style.css +160 -0
- package/src/tagUtils.js +148 -0
- package/src/team.data.js +1 -1
- package/src/vitepressConfig.mjs +97 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section class="testimonials-section full-width surface-container-low">
|
|
3
|
+
<div class="holder">
|
|
4
|
+
<!-- Left Side: Headline -->
|
|
5
|
+
<div class="headline">
|
|
6
|
+
<h2>
|
|
7
|
+
{{ title }}
|
|
8
|
+
</h2>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<!-- Right Side: Text Block -->
|
|
12
|
+
<div v-if="testimonials && testimonials.length > 0" class="text-block">
|
|
13
|
+
<!-- Testimonial Quote -->
|
|
14
|
+
<div class="quote-text">
|
|
15
|
+
“{{ currentTestimonial.quote }}”
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Divider / Separator -->
|
|
19
|
+
<div class="divider"></div>
|
|
20
|
+
|
|
21
|
+
<!-- Frame 469: Author info & sliders -->
|
|
22
|
+
<div class="frame-469">
|
|
23
|
+
<!-- Frame 468: Author Name and Role -->
|
|
24
|
+
<div class="frame-468">
|
|
25
|
+
<span class="author-name">
|
|
26
|
+
{{ currentTestimonial.author }}
|
|
27
|
+
</span>
|
|
28
|
+
<span class="author-role">
|
|
29
|
+
{{ currentTestimonial.role }}
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- sliders: Navigation Buttons -->
|
|
34
|
+
<div class="sliders">
|
|
35
|
+
<!-- Left Arrow Button -->
|
|
36
|
+
<button
|
|
37
|
+
class="nav-button"
|
|
38
|
+
@click="prev"
|
|
39
|
+
aria-label="Previous testimonial"
|
|
40
|
+
>
|
|
41
|
+
<i class="mdi mdi-arrow-left"></i>
|
|
42
|
+
</button>
|
|
43
|
+
|
|
44
|
+
<!-- Right Arrow Button -->
|
|
45
|
+
<button
|
|
46
|
+
class="nav-button"
|
|
47
|
+
@click="next"
|
|
48
|
+
aria-label="Next testimonial"
|
|
49
|
+
>
|
|
50
|
+
<i class="mdi mdi-arrow-right"></i>
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</section>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<script setup>
|
|
60
|
+
import { ref, computed } from "vue";
|
|
61
|
+
|
|
62
|
+
const props = defineProps({
|
|
63
|
+
title: {
|
|
64
|
+
type: String,
|
|
65
|
+
default: "What our customers are saying",
|
|
66
|
+
},
|
|
67
|
+
testimonials: {
|
|
68
|
+
type: Array,
|
|
69
|
+
default: () => [
|
|
70
|
+
{
|
|
71
|
+
quote:
|
|
72
|
+
"The VirES platform has completely revolutionized how Swarm mission data is accessed and analyzed by the global scientific community. Its cloud-native performance and seamless visualization are unmatched.",
|
|
73
|
+
author: "Dr. Fiona Harrison",
|
|
74
|
+
role: "Lead Swarm Researcher, Scientific Advisory Group",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
quote:
|
|
78
|
+
"Deploying our Area Monitoring System on EOxHub workspaces allowed us to process millions of parcels in near real-time. This level of automation and reliability is precisely what we needed for the CAP implementation.",
|
|
79
|
+
author: "Dr. Liam O'Connor",
|
|
80
|
+
role: "Senior Spatial Scientist, Agricultural Paying Agency",
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
quote:
|
|
84
|
+
"The EOX base map layers are an essential asset for our global dashboard. Their Sentinel-2 cloudless composites provide a pristine, cloud-free visual experience that our users absolutely love.",
|
|
85
|
+
author: "Sarah Jenkins",
|
|
86
|
+
role: "Chief Product Officer, Global Earth Explorer Platform",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const currentIndex = ref(0);
|
|
93
|
+
|
|
94
|
+
const currentTestimonial = computed(() => {
|
|
95
|
+
if (!props.testimonials || props.testimonials.length === 0) return null;
|
|
96
|
+
return props.testimonials[currentIndex.value];
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function prev() {
|
|
100
|
+
if (!props.testimonials || props.testimonials.length === 0) return;
|
|
101
|
+
currentIndex.value =
|
|
102
|
+
(currentIndex.value - 1 + props.testimonials.length) %
|
|
103
|
+
props.testimonials.length;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function next() {
|
|
107
|
+
if (!props.testimonials || props.testimonials.length === 0) return;
|
|
108
|
+
currentIndex.value = (currentIndex.value + 1) % props.testimonials.length;
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
section.testimonials-section {
|
|
114
|
+
display: flex;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
overflow-x: hidden;
|
|
117
|
+
padding: 3rem 1.5rem;
|
|
118
|
+
margin-block-start: 6rem;
|
|
119
|
+
margin-block-end: 6rem;
|
|
120
|
+
box-sizing: border-box;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@media (min-width: 1024px) {
|
|
124
|
+
section.testimonials-section {
|
|
125
|
+
padding: 6rem 1.5rem;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.holder {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-direction: column;
|
|
132
|
+
gap: 32px;
|
|
133
|
+
width: 100%;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@media (min-width: 1024px) {
|
|
137
|
+
.holder {
|
|
138
|
+
flex-direction: row;
|
|
139
|
+
align-items: flex-start;
|
|
140
|
+
justify-content: space-between;
|
|
141
|
+
gap: 64px;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.headline {
|
|
146
|
+
flex: 1 1 50%;
|
|
147
|
+
max-width: 620px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.headline h2 {
|
|
151
|
+
font-family: "Inter", sans-serif;
|
|
152
|
+
font-style: normal;
|
|
153
|
+
font-weight: 700;
|
|
154
|
+
font-size: 36px !important;
|
|
155
|
+
line-height: 44px;
|
|
156
|
+
letter-spacing: 0.5008px;
|
|
157
|
+
color: #00060a;
|
|
158
|
+
margin: 0 !important;
|
|
159
|
+
padding: 0 !important;
|
|
160
|
+
border-top: none !important;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.text-block {
|
|
164
|
+
flex: 1 1 50%;
|
|
165
|
+
max-width: 620px;
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
gap: 24px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.quote-text {
|
|
172
|
+
font-family: "Inter", sans-serif;
|
|
173
|
+
font-style: normal;
|
|
174
|
+
font-weight: 400;
|
|
175
|
+
font-size: 16px;
|
|
176
|
+
line-height: 24px;
|
|
177
|
+
letter-spacing: 0.5px;
|
|
178
|
+
color: #00060a;
|
|
179
|
+
min-height: 72px;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.divider {
|
|
183
|
+
width: 100%;
|
|
184
|
+
height: 1px;
|
|
185
|
+
background: #ccd9e2;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.frame-469 {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: row;
|
|
191
|
+
align-items: center;
|
|
192
|
+
justify-content: space-between;
|
|
193
|
+
gap: 16px;
|
|
194
|
+
width: 100%;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.frame-468 {
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
align-items: flex-start;
|
|
201
|
+
gap: 8px;
|
|
202
|
+
flex-grow: 1;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.author-name {
|
|
206
|
+
font-family: "Inter", sans-serif;
|
|
207
|
+
font-style: normal;
|
|
208
|
+
font-weight: 700;
|
|
209
|
+
font-size: 16px;
|
|
210
|
+
line-height: 24px;
|
|
211
|
+
letter-spacing: 0.5px;
|
|
212
|
+
color: #00060a;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.author-role {
|
|
216
|
+
font-family: "Inter", sans-serif;
|
|
217
|
+
font-style: normal;
|
|
218
|
+
font-weight: 400;
|
|
219
|
+
font-size: 16px;
|
|
220
|
+
line-height: 24px;
|
|
221
|
+
letter-spacing: 0.5px;
|
|
222
|
+
color: #00060a;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.sliders {
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: row;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 24px;
|
|
230
|
+
flex-shrink: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.nav-button {
|
|
234
|
+
display: flex !important;
|
|
235
|
+
flex-direction: row !important;
|
|
236
|
+
justify-content: center !important;
|
|
237
|
+
align-items: center !important;
|
|
238
|
+
padding: 0 !important;
|
|
239
|
+
width: 48px !important;
|
|
240
|
+
min-width: 48px !important;
|
|
241
|
+
max-width: 48px !important;
|
|
242
|
+
height: 48px !important;
|
|
243
|
+
min-height: 48px !important;
|
|
244
|
+
max-height: 48px !important;
|
|
245
|
+
background: #004170 !important;
|
|
246
|
+
box-shadow:
|
|
247
|
+
0px 24px 48px -8px rgba(62, 52, 69, 0.12),
|
|
248
|
+
0px 2px 4px rgba(62, 52, 69, 0.04) !important;
|
|
249
|
+
border-radius: 999px !important;
|
|
250
|
+
border: none !important;
|
|
251
|
+
cursor: pointer !important;
|
|
252
|
+
transition: background-color 0.2s ease !important;
|
|
253
|
+
box-sizing: border-box !important;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.nav-button:hover {
|
|
257
|
+
background: #002e52;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.nav-button i {
|
|
261
|
+
color: #ffffff;
|
|
262
|
+
font-size: 24px;
|
|
263
|
+
line-height: 1;
|
|
264
|
+
}
|
|
265
|
+
</style>
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,12 @@ import ImageElement from "./components/ImageElement.vue";
|
|
|
17
17
|
import ImageGallery from "./components/ImageGallery.vue";
|
|
18
18
|
import ImageCarousel from "./components/ImageCarousel.vue";
|
|
19
19
|
import SingleTweet from "./components/SingleTweet.vue";
|
|
20
|
+
import BlogPost from "./components/BlogPost.vue";
|
|
21
|
+
import TagList from "./components/TagList.vue";
|
|
22
|
+
import BlogList from "./components/BlogList.vue";
|
|
23
|
+
import TeamPage from "./components/TeamPage.vue";
|
|
24
|
+
import SectionHeader from "./components/SectionHeader.vue";
|
|
25
|
+
import TestimonialsSection from "./components/TestimonialsSection.vue";
|
|
20
26
|
import Layout from "./Layout.vue";
|
|
21
27
|
import "./style.css";
|
|
22
28
|
|
|
@@ -43,6 +49,12 @@ export default {
|
|
|
43
49
|
app.component("GalleryGrid", ImageGallery);
|
|
44
50
|
app.component("ImageCarousel", ImageCarousel);
|
|
45
51
|
app.component("SingleTweet", SingleTweet);
|
|
52
|
+
app.component("BlogPost", BlogPost);
|
|
53
|
+
app.component("TagList", TagList);
|
|
54
|
+
app.component("BlogList", BlogList);
|
|
55
|
+
app.component("TeamPage", TeamPage);
|
|
56
|
+
app.component("SectionHeader", SectionHeader);
|
|
57
|
+
app.component("TestimonialsSection", TestimonialsSection);
|
|
46
58
|
|
|
47
59
|
router.onAfterRouteChanged = () => {
|
|
48
60
|
if (!import.meta.env.SSR) {
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
paths() {
|
|
6
|
+
const postsDir = path.resolve(process.cwd(), "_posts");
|
|
7
|
+
if (!fs.existsSync(postsDir)) return [];
|
|
8
|
+
|
|
9
|
+
const files = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
|
|
10
|
+
const allAuthors = new Set();
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const filePath = path.join(postsDir, file);
|
|
14
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
15
|
+
|
|
16
|
+
// Extract yaml frontmatter block
|
|
17
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (frontmatterMatch) {
|
|
19
|
+
const yaml = frontmatterMatch[1];
|
|
20
|
+
|
|
21
|
+
// 1. Match inline array or string style author
|
|
22
|
+
const authorMatch = yaml.match(/author:\s*(.*?)\r?\n/);
|
|
23
|
+
if (authorMatch) {
|
|
24
|
+
const val = authorMatch[1].trim();
|
|
25
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
26
|
+
val
|
|
27
|
+
.slice(1, -1)
|
|
28
|
+
.split(",")
|
|
29
|
+
.forEach((a) => {
|
|
30
|
+
const clean = a.trim().replace(/['"]/g, "").toLowerCase();
|
|
31
|
+
if (clean) allAuthors.add(clean);
|
|
32
|
+
});
|
|
33
|
+
} else if (val) {
|
|
34
|
+
const clean = val.replace(/['"]/g, "").toLowerCase();
|
|
35
|
+
if (clean) allAuthors.add(clean);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Match block list style authors
|
|
40
|
+
const authorBlockMatch = yaml.match(
|
|
41
|
+
/author:\r?\n((?:\s*-\s*.*\r?\n?)+)/,
|
|
42
|
+
);
|
|
43
|
+
if (authorBlockMatch) {
|
|
44
|
+
const lines = authorBlockMatch[1].split(/\r?\n/);
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const m = line.match(/^\s*-\s*["']?(.*?)["']?\s*$/);
|
|
47
|
+
if (m && m[1]) {
|
|
48
|
+
const auth = m[1].trim().toLowerCase();
|
|
49
|
+
if (auth) allAuthors.add(auth);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Array.from(allAuthors).map((author) => {
|
|
57
|
+
return { params: { author } };
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
paths() {
|
|
6
|
+
const postsDir = path.resolve(process.cwd(), "_posts");
|
|
7
|
+
if (!fs.existsSync(postsDir)) return [];
|
|
8
|
+
|
|
9
|
+
const files = fs.readdirSync(postsDir).filter((f) => f.endsWith(".md"));
|
|
10
|
+
const allTags = new Set();
|
|
11
|
+
|
|
12
|
+
for (const file of files) {
|
|
13
|
+
const filePath = path.join(postsDir, file);
|
|
14
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
15
|
+
|
|
16
|
+
// Robust YAML frontmatter parser
|
|
17
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
18
|
+
if (frontmatterMatch) {
|
|
19
|
+
const yaml = frontmatterMatch[1];
|
|
20
|
+
|
|
21
|
+
// 1. Match list-style tags
|
|
22
|
+
const tagsBlockMatch = yaml.match(/tags:\r?\n((?:\s*-\s*.*\r?\n?)+)/);
|
|
23
|
+
if (tagsBlockMatch) {
|
|
24
|
+
const lines = tagsBlockMatch[1].split(/\r?\n/);
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const m = line.match(/^\s*-\s*["']?(.*?)["']?\s*$/);
|
|
27
|
+
if (m && m[1]) {
|
|
28
|
+
const tag = m[1].trim().toLowerCase();
|
|
29
|
+
if (tag) allTags.add(tag);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Match inline-array style tags
|
|
35
|
+
const inlineMatch = yaml.match(/tags:\s*\[([\s\S]*?)\]/);
|
|
36
|
+
if (inlineMatch) {
|
|
37
|
+
inlineMatch[1].split(",").forEach((t) => {
|
|
38
|
+
const tag = t.trim().replace(/['"]/g, "").toLowerCase();
|
|
39
|
+
if (tag) allTags.add(tag);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return Array.from(allTags).map((tag) => {
|
|
46
|
+
return { params: { tag } };
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
};
|
package/src/pages/tag.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createContentLoader } from "vitepress";
|
|
2
|
+
|
|
3
|
+
function cleanMarkdown(src) {
|
|
4
|
+
if (!src) return "";
|
|
5
|
+
|
|
6
|
+
// 1. If there's a <!-- more --> tag, truncate there
|
|
7
|
+
const moreIndex = src.indexOf("<!-- more -->");
|
|
8
|
+
let content = moreIndex !== -1 ? src.slice(0, moreIndex) : src;
|
|
9
|
+
|
|
10
|
+
// 2. Remove frontmatter (just in case)
|
|
11
|
+
if (content.startsWith("---")) {
|
|
12
|
+
const nextThree = content.indexOf("---", 3);
|
|
13
|
+
if (nextThree !== -1) {
|
|
14
|
+
content = content.slice(nextThree + 3);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// 3. Remove standard markdown and HTML elements
|
|
19
|
+
let text = content
|
|
20
|
+
// Remove custom containers like ::: info or ::: warning ... :::
|
|
21
|
+
.replace(/:::[^\n]*\n/g, "")
|
|
22
|
+
.replace(/:::/g, "")
|
|
23
|
+
// Remove HTML comments
|
|
24
|
+
.replace(/<!--[\s\S]*?-->/g, "")
|
|
25
|
+
// Remove images: 
|
|
26
|
+
.replace(/!\[.*?\]\(.*?\)/g, "")
|
|
27
|
+
// Remove links but keep text: [text](url)
|
|
28
|
+
.replace(/\[(.*?)\]\(.*?\)/g, "$1")
|
|
29
|
+
// Remove blockquotes: > quote
|
|
30
|
+
.replace(/^\s*>\s+/gm, "")
|
|
31
|
+
// Clean up lists: * list item, - list item, 1. list item
|
|
32
|
+
.replace(/^\s*[\*+-]\s+/gm, "")
|
|
33
|
+
.replace(/^\s*\d+\.\s+/gm, "")
|
|
34
|
+
// Remove tables/borders
|
|
35
|
+
.replace(/\|/g, "")
|
|
36
|
+
.replace(/[-]{3,}/g, "")
|
|
37
|
+
// Remove custom components / HTML tags but keep nested content
|
|
38
|
+
.replace(/<[^>]+>/g, "")
|
|
39
|
+
// Remove bold/italic formatting
|
|
40
|
+
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
|
41
|
+
.replace(/(\*|_)(.*?)\1/g, "$2")
|
|
42
|
+
// Remove inline code
|
|
43
|
+
.replace(/`(.*?)`/g, "$1")
|
|
44
|
+
// Replace multiple newlines or spaces with a single space
|
|
45
|
+
.replace(/\s+/g, " ")
|
|
46
|
+
.trim();
|
|
47
|
+
|
|
48
|
+
return text;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getExcerpt(src, maxLength = 200) {
|
|
52
|
+
const text = cleanMarkdown(src);
|
|
53
|
+
if (text.length > maxLength) {
|
|
54
|
+
// Try to truncate at a word boundary within the last 30 characters
|
|
55
|
+
const truncated = text.slice(0, maxLength);
|
|
56
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
57
|
+
if (lastSpace > maxLength - 30) {
|
|
58
|
+
return truncated.slice(0, lastSpace) + " ...";
|
|
59
|
+
}
|
|
60
|
+
return truncated + " ...";
|
|
61
|
+
}
|
|
62
|
+
return text;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default createContentLoader("_posts/*.md", {
|
|
66
|
+
includeSrc: true,
|
|
67
|
+
transform(rawData) {
|
|
68
|
+
return rawData
|
|
69
|
+
.map((page) => {
|
|
70
|
+
// Normalize single author / authors array
|
|
71
|
+
const author = page.frontmatter.author || "";
|
|
72
|
+
const authors = Array.isArray(author) ? author : [author];
|
|
73
|
+
|
|
74
|
+
const excerpt =
|
|
75
|
+
page.frontmatter.description ||
|
|
76
|
+
page.frontmatter.summary ||
|
|
77
|
+
getExcerpt(page.src);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
title: page.frontmatter.title || "",
|
|
81
|
+
url: page.url,
|
|
82
|
+
date: page.frontmatter.date || "",
|
|
83
|
+
authors,
|
|
84
|
+
tags: page.frontmatter.tags || [],
|
|
85
|
+
categories: page.frontmatter.categories || [],
|
|
86
|
+
coverImage:
|
|
87
|
+
page.frontmatter.coverImage || page.frontmatter.image || "",
|
|
88
|
+
excerpt,
|
|
89
|
+
};
|
|
90
|
+
})
|
|
91
|
+
.sort((a, b) => {
|
|
92
|
+
// Sort descending by date (newest first)
|
|
93
|
+
return b.date.localeCompare(a.date);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
});
|
package/src/style.css
CHANGED
|
@@ -86,6 +86,12 @@ body {
|
|
|
86
86
|
padding-right: 0;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* Ensure full-width layout sections touch and stack seamlessly without gaps */
|
|
90
|
+
.VPHome .full-width:not(.home-hero-section) {
|
|
91
|
+
margin-block-start: 0 !important;
|
|
92
|
+
margin-block-end: 0 !important;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
.VPHome .vp-doc a {
|
|
90
96
|
font-weight: 400;
|
|
91
97
|
color: inherit;
|
|
@@ -345,3 +351,157 @@ eox-feedback-button {
|
|
|
345
351
|
z-index: calc(var(--vp-z-index-sidebar) + 2);
|
|
346
352
|
}
|
|
347
353
|
}
|
|
354
|
+
|
|
355
|
+
/* ==========================================================================
|
|
356
|
+
Simple Pages (layout: page) styling improvements
|
|
357
|
+
========================================================================== */
|
|
358
|
+
|
|
359
|
+
.layout-page {
|
|
360
|
+
background-color: var(--surface) !important;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.layout-page .VPPage {
|
|
364
|
+
max-width: 1200px;
|
|
365
|
+
margin-inline: auto;
|
|
366
|
+
padding-block-start: 66px; /* Exactly 156px from top of viewport (90px navbar + 66px padding) */
|
|
367
|
+
padding-block-end: 96px;
|
|
368
|
+
padding-inline: 32px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/* Remove double inner padding on custom layout elements to ensure absolute alignment */
|
|
372
|
+
.layout-page .VPPage .holder {
|
|
373
|
+
padding-inline: 0 !important;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Constrain standard reading elements to 1000px max-width, while letting full-width custom page components span 1200px */
|
|
377
|
+
.layout-page .VPPage > div > div > :is(p, ul, ol, blockquote, h1, h2, h3, h4, h5, h6, pre, table) {
|
|
378
|
+
max-width: 1000px;
|
|
379
|
+
margin-inline-start: 0;
|
|
380
|
+
margin-inline-end: auto;
|
|
381
|
+
width: 100%;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
@media (max-width: 960px) {
|
|
385
|
+
.layout-page .VPPage {
|
|
386
|
+
padding-inline: 24px;
|
|
387
|
+
padding-block-start: 48px;
|
|
388
|
+
padding-block-end: 64px;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
@media (max-width: 640px) {
|
|
393
|
+
.layout-page .VPPage {
|
|
394
|
+
padding-inline: 16px;
|
|
395
|
+
padding-block-start: 36px;
|
|
396
|
+
padding-block-end: 48px;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Typography on simple pages inside main page content */
|
|
401
|
+
.layout-page .VPPage h1 {
|
|
402
|
+
font-size: 2rem; /* Exactly 32px */
|
|
403
|
+
line-height: 1.25;
|
|
404
|
+
margin-block-start: 0;
|
|
405
|
+
margin-block-end: 1.5rem;
|
|
406
|
+
color: var(--on-surface);
|
|
407
|
+
font-weight: 700;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
@media (max-width: 640px) {
|
|
411
|
+
.layout-page .VPPage h1 {
|
|
412
|
+
font-size: 1.75rem; /* 28px on mobile */
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.layout-page .VPPage h2 {
|
|
417
|
+
font-size: 1.5rem; /* Exactly 24px */
|
|
418
|
+
line-height: 1.35;
|
|
419
|
+
margin-block-start: 3rem;
|
|
420
|
+
margin-block-end: 1.125rem;
|
|
421
|
+
color: var(--on-surface);
|
|
422
|
+
border-top: none;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
@media (max-width: 640px) {
|
|
426
|
+
.layout-page .VPPage h2 {
|
|
427
|
+
font-size: 1.375rem; /* 22px on mobile */
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.layout-page .VPPage h3 {
|
|
432
|
+
font-size: clamp(1.2rem, 3.5vw, 1.45rem);
|
|
433
|
+
line-height: 1.3;
|
|
434
|
+
margin-block-start: 2.25rem;
|
|
435
|
+
margin-block-end: 0.875rem;
|
|
436
|
+
color: var(--on-surface);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
.layout-page .VPPage h4 {
|
|
440
|
+
font-size: 1.15rem;
|
|
441
|
+
line-height: 1.4;
|
|
442
|
+
margin-block-start: 1.75rem;
|
|
443
|
+
margin-block-end: 0.75rem;
|
|
444
|
+
color: var(--on-surface);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.layout-page .VPPage p {
|
|
448
|
+
font-size: 1rem; /* Exactly 16px */
|
|
449
|
+
line-height: 1.65;
|
|
450
|
+
margin-block-start: 0;
|
|
451
|
+
margin-block-end: 1.5rem;
|
|
452
|
+
color: var(--on-surface);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* Lists */
|
|
456
|
+
.layout-page .VPPage ul,
|
|
457
|
+
.layout-page .VPPage ol {
|
|
458
|
+
margin-block-start: 0;
|
|
459
|
+
margin-block-end: 1.5rem;
|
|
460
|
+
padding-inline-start: 1.5rem;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.layout-page .VPPage li {
|
|
464
|
+
margin-block-end: 0.5rem;
|
|
465
|
+
line-height: 1.6;
|
|
466
|
+
font-size: 1rem; /* Exactly 16px */
|
|
467
|
+
color: var(--on-surface);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* Inline links readability fix */
|
|
471
|
+
.layout-page .VPPage a:not(.button) {
|
|
472
|
+
color: var(--primary);
|
|
473
|
+
text-decoration: underline;
|
|
474
|
+
text-underline-offset: 3px;
|
|
475
|
+
text-decoration-thickness: 1px;
|
|
476
|
+
transition: color 0.2s ease, text-decoration-color 0.2s ease;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.layout-page .VPPage a:not(.button):hover {
|
|
480
|
+
opacity: 0.85;
|
|
481
|
+
text-decoration-thickness: 2px;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* Scroll Anchors, Blockquotes & Code */
|
|
485
|
+
.layout-page .VPPage blockquote {
|
|
486
|
+
border-inline-start: 4px solid var(--primary);
|
|
487
|
+
padding-inline-start: 1.25rem;
|
|
488
|
+
margin-inline: 0;
|
|
489
|
+
margin-block: 2rem;
|
|
490
|
+
font-style: italic;
|
|
491
|
+
color: var(--on-surface-variant);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.layout-page .VPPage sup,
|
|
495
|
+
.layout-page .VPPage sub {
|
|
496
|
+
font-size: 0.8rem;
|
|
497
|
+
line-height: 1;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.layout-page .VPPage pre {
|
|
501
|
+
margin-block: 1.5rem;
|
|
502
|
+
padding: 1rem;
|
|
503
|
+
border-radius: 8px;
|
|
504
|
+
background-color: var(--surface-container-low);
|
|
505
|
+
overflow-x: auto;
|
|
506
|
+
}
|
|
507
|
+
|