@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,4 @@
1
+ ---
2
+ layout: blog
3
+ title: Author posts
4
+ ---
@@ -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,4 @@
1
+ ---
2
+ layout: blog
3
+ title: Blog
4
+ ---
@@ -0,0 +1,4 @@
1
+ ---
2
+ layout: blog
3
+ title: Tag list
4
+ ---
@@ -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
+ };
@@ -0,0 +1,4 @@
1
+ ---
2
+ layout: tags
3
+ title: Tags
4
+ ---
@@ -0,0 +1,4 @@
1
+ ---
2
+ layout: team
3
+ title: Team
4
+ ---
@@ -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: ![alt](url)
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
+
@@ -0,0 +1,148 @@
1
+ export const tagThemes = [
2
+ // Managed cloud workspace (Category 0)
3
+ [
4
+ "eoxhub",
5
+ "eurodatacube",
6
+ "cloud-processing",
7
+ "mundi",
8
+ "jupyter",
9
+ "vre",
10
+ "processing",
11
+ ],
12
+ // Data management (Category 1)
13
+ [
14
+ "data-access",
15
+ "catalog",
16
+ "eo-data",
17
+ "open-search",
18
+ "ard",
19
+ "eoxserver",
20
+ "django",
21
+ "sxcat",
22
+ "data-cubes",
23
+ "opensearch",
24
+ ],
25
+ // Data visualization (Category 2)
26
+ [
27
+ "3d",
28
+ "animation",
29
+ "cesium",
30
+ "globe",
31
+ "ndvi",
32
+ "plotty",
33
+ "graphly",
34
+ "geotiff-js",
35
+ "client",
36
+ "visualization",
37
+ "cog",
38
+ "geotiff",
39
+ "openlayers",
40
+ "vue-js",
41
+ "eoxc",
42
+ "dashboard",
43
+ "javascript",
44
+ "leaflet",
45
+ "d3",
46
+ "webgl",
47
+ "video",
48
+ "data visualization",
49
+ ],
50
+ // Data products (Category 3)
51
+ [
52
+ "eoxcloudless",
53
+ "eoxmaps",
54
+ "s2cloudless",
55
+ "maps",
56
+ "sentinel-2",
57
+ "labels",
58
+ "cloudless-mosaic",
59
+ "voronoi-diagram",
60
+ "openstreetmap",
61
+ "mapchete",
62
+ ],
63
+ ];
64
+
65
+ export const tagColors = [
66
+ "rgb(255, 94, 94)", // Managed cloud workspace
67
+ "rgb(0, 177, 176)", // Data management
68
+ "rgb(219, 173, 66)", // Data visualization
69
+ "rgb(218, 71, 228)", // Data products
70
+ ];
71
+
72
+ export const tagCategories = [
73
+ "Managed cloud workspace",
74
+ "Data management",
75
+ "Data visualization",
76
+ "Data products",
77
+ ];
78
+
79
+ /**
80
+ * Get color for a given tag
81
+ * @param {string} tag
82
+ * @returns {string} color hex/rgb
83
+ */
84
+ export function getTagColor(tag) {
85
+ if (!tag) return "#e0e0e0";
86
+ const cleanTag = tag.toLowerCase().trim();
87
+ for (let i = 0; i < tagThemes.length; i++) {
88
+ if (tagThemes[i].includes(cleanTag)) {
89
+ return tagColors[i];
90
+ }
91
+ }
92
+ return "#e0e0e0";
93
+ }
94
+
95
+ /**
96
+ * Get style object for a given tag
97
+ * @param {string} tag
98
+ * @returns {object} style object
99
+ */
100
+ export function getTagStyle(tag) {
101
+ const color = getTagColor(tag);
102
+ if (color.startsWith("rgb(255, 94, 94)")) {
103
+ return {
104
+ backgroundColor: "rgba(255, 94, 94, 0.15)",
105
+ border: "1px solid rgb(255, 94, 94)",
106
+ color: "#d32f2f"
107
+ };
108
+ } else if (color.startsWith("rgb(0, 177, 176)")) {
109
+ return {
110
+ backgroundColor: "rgba(0, 177, 176, 0.15)",
111
+ border: "1px solid rgb(0, 177, 176)",
112
+ color: "#00796b"
113
+ };
114
+ } else if (color.startsWith("rgb(219, 173, 66)")) {
115
+ return {
116
+ backgroundColor: "rgba(219, 173, 66, 0.15)",
117
+ border: "1px solid rgb(219, 173, 66)",
118
+ color: "#f57c00"
119
+ };
120
+ } else if (color.startsWith("rgb(218, 71, 228)")) {
121
+ return {
122
+ backgroundColor: "rgba(218, 71, 228, 0.15)",
123
+ border: "1px solid rgb(218, 71, 228)",
124
+ color: "#c2185b"
125
+ };
126
+ }
127
+ return {
128
+ backgroundColor: "rgba(0, 0, 0, 0.05)",
129
+ border: "1px solid rgba(0, 0, 0, 0.1)",
130
+ color: "#475569"
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Get category name for a given tag
136
+ * @param {string} tag
137
+ * @returns {string} category name
138
+ */
139
+ export function getTagCategory(tag) {
140
+ if (!tag) return "Other";
141
+ const cleanTag = tag.toLowerCase().trim();
142
+ for (let i = 0; i < tagThemes.length; i++) {
143
+ if (tagThemes[i].includes(cleanTag)) {
144
+ return tagCategories[i];
145
+ }
146
+ }
147
+ return "Other";
148
+ }
@@ -0,0 +1,51 @@
1
+ import { createContentLoader } from "vitepress";
2
+
3
+ export default createContentLoader("_team/*.md", {
4
+ transform(rawData) {
5
+ return rawData
6
+ .filter((page) => {
7
+ // Exclude guest members
8
+ return !(page.frontmatter.guest === true);
9
+ })
10
+ .map((page) => {
11
+ // Find job description inside the meta array
12
+ const metaList = page.frontmatter.meta || [];
13
+ const jobDescMeta = Array.isArray(metaList)
14
+ ? metaList.find((m) => m && m.name === "jobDescription")
15
+ : null;
16
+ const role = jobDescMeta ? jobDescMeta.content : "";
17
+
18
+ // Transform sns array of objects into a single object of key-value pairs
19
+ const sns = {};
20
+ const snsList = page.frontmatter.sns || [];
21
+ if (Array.isArray(snsList)) {
22
+ snsList.forEach((item) => {
23
+ if (item && typeof item === "object") {
24
+ Object.entries(item).forEach(([platform, url]) => {
25
+ sns[platform] = url;
26
+ });
27
+ }
28
+ });
29
+ }
30
+
31
+ // Return the clean schema
32
+ return {
33
+ name: page.frontmatter.title || "",
34
+ id: page.frontmatter.id || "",
35
+ image: page.frontmatter.image || "",
36
+ bio: page.frontmatter.description || "",
37
+ role,
38
+ sns,
39
+ url: page.url,
40
+ };
41
+ })
42
+ .sort((a, b) => {
43
+ // "0_new" (id: new_member) should be in the 1st position.
44
+ if (a.id === "new_member") return -1;
45
+ if (b.id === "new_member") return 1;
46
+
47
+ // Otherwise sort alphabetically by ID (which maps to filename / last name)
48
+ return a.id.localeCompare(b.id);
49
+ });
50
+ },
51
+ });