@eox/pages-theme-eox 0.10.1 → 0.11.0
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/config.mjs +3 -1
- package/package.json +2 -2
- package/src/components/DataTable.vue +143 -0
- package/src/components/FeatureCard.vue +69 -0
- package/src/components/FeaturesGallery.vue +102 -54
- package/src/index.js +2 -0
package/config.mjs
CHANGED
|
@@ -10,7 +10,9 @@ export default async (brandId = "eox") => {
|
|
|
10
10
|
register(pathToFileURL(path.join(__dirname, "./src/https-hooks.mjs")));
|
|
11
11
|
|
|
12
12
|
// Load the remote brand configuration
|
|
13
|
-
const brand = await import(
|
|
13
|
+
const brand = await import(
|
|
14
|
+
`https://cdn-hub-brands.ext.eox.at/${brandId}/config.mjs`
|
|
15
|
+
);
|
|
14
16
|
|
|
15
17
|
// Load the base VitePress configuration
|
|
16
18
|
const vitepressConfig = await import("./src/vitepressConfig.mjs");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eox/pages-theme-eox",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vitepress Theme with EOX branding",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@eox/eslint-config": "^2.0.0",
|
|
14
|
-
"@eox/ui": "^0.3.
|
|
14
|
+
"@eox/ui": "^0.3.7",
|
|
15
15
|
"vitepress": "^1.6.3"
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { ref } from "vue";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} TableRowData
|
|
6
|
+
* @property {Object.<string, string>} summary - Key-value pairs for each column header
|
|
7
|
+
* @property {string} content - Detailed content shown when row is expanded
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} TableProps
|
|
12
|
+
* @property {string[]} headers - Array of column header names
|
|
13
|
+
* @property {TableRowData[]} data - Array of row data objects
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Props
|
|
17
|
+
defineProps({
|
|
18
|
+
headers: {
|
|
19
|
+
/** @type {import('vue').PropType<string[]>} Array of column header names */
|
|
20
|
+
type: Array,
|
|
21
|
+
required: true,
|
|
22
|
+
default: () => [],
|
|
23
|
+
},
|
|
24
|
+
data: {
|
|
25
|
+
/** @type {import("vue").PropType<TableRowData[]>} Array of row data objects with summary and content */
|
|
26
|
+
type: Array,
|
|
27
|
+
required: true,
|
|
28
|
+
default: () => [],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// State for tracking expanded rows
|
|
33
|
+
const expandedRows = ref(new Set());
|
|
34
|
+
/**
|
|
35
|
+
*
|
|
36
|
+
* @param {number} index
|
|
37
|
+
*/
|
|
38
|
+
// Toggle row expansion
|
|
39
|
+
const toggleRow = (index) => {
|
|
40
|
+
if (expandedRows.value.has(index)) {
|
|
41
|
+
expandedRows.value.delete(index);
|
|
42
|
+
} else {
|
|
43
|
+
expandedRows.value.add(index);
|
|
44
|
+
}
|
|
45
|
+
// Trigger reactivity
|
|
46
|
+
expandedRows.value = new Set(expandedRows.value);
|
|
47
|
+
};
|
|
48
|
+
</script>
|
|
49
|
+
<template>
|
|
50
|
+
<table class="medium-space">
|
|
51
|
+
<thead>
|
|
52
|
+
<tr>
|
|
53
|
+
<th v-for="header in headers" :key="header">
|
|
54
|
+
{{ header }}
|
|
55
|
+
</th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
<template v-for="(row, index) in data" :key="index">
|
|
60
|
+
<!-- Main row -->
|
|
61
|
+
<tr
|
|
62
|
+
class="clickable hover"
|
|
63
|
+
@click="toggleRow(index)"
|
|
64
|
+
:class="{ active: expandedRows.has(index) }"
|
|
65
|
+
>
|
|
66
|
+
<td
|
|
67
|
+
v-for="header in headers"
|
|
68
|
+
:key="header"
|
|
69
|
+
v-html="row.summary[header] || ''"
|
|
70
|
+
></td>
|
|
71
|
+
</tr>
|
|
72
|
+
<!-- Expanded content row -->
|
|
73
|
+
<tr v-if="expandedRows.has(index)">
|
|
74
|
+
<td :colspan="headers.length">
|
|
75
|
+
<div v-html="row.content"></div>
|
|
76
|
+
</td>
|
|
77
|
+
</tr>
|
|
78
|
+
</template>
|
|
79
|
+
</tbody>
|
|
80
|
+
</table>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<style scoped>
|
|
84
|
+
/**
|
|
85
|
+
* Undo Vitepress missing with @eox/ui styles
|
|
86
|
+
*/
|
|
87
|
+
.vp-doc table {
|
|
88
|
+
display: table;
|
|
89
|
+
}
|
|
90
|
+
.vp-doc th {
|
|
91
|
+
background: initial;
|
|
92
|
+
}
|
|
93
|
+
.vp-doc tr:nth-child(2n) {
|
|
94
|
+
background-color: initial;
|
|
95
|
+
}
|
|
96
|
+
.vp-doc table,
|
|
97
|
+
.vp-doc th,
|
|
98
|
+
.vp-doc tr,
|
|
99
|
+
.vp-doc td {
|
|
100
|
+
background: transparent;
|
|
101
|
+
border: none;
|
|
102
|
+
color: initial;
|
|
103
|
+
}
|
|
104
|
+
.vp-doc tr,
|
|
105
|
+
.vp-doc td {
|
|
106
|
+
font-size: 1rem;
|
|
107
|
+
}
|
|
108
|
+
table.border > tbody > tr:not(:last-child) > td,
|
|
109
|
+
thead > tr > th {
|
|
110
|
+
border-block-end: 0.0625rem solid var(--outline) !important;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Custom styles
|
|
115
|
+
*/
|
|
116
|
+
table th {
|
|
117
|
+
font-size: 1.25rem;
|
|
118
|
+
}
|
|
119
|
+
.clickable {
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
transition: background-color 0.2s ease;
|
|
122
|
+
font-weight: 600;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.clickable:hover {
|
|
126
|
+
background-color: var(--surface-variant) !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.clickable.active {
|
|
130
|
+
background-color: var(--surface-variant) !important;
|
|
131
|
+
border-bottom: none;
|
|
132
|
+
}
|
|
133
|
+
.clickable.active > td {
|
|
134
|
+
border: none;
|
|
135
|
+
}
|
|
136
|
+
.clickable.active + tr {
|
|
137
|
+
background-color: var(--surface-variant) !important;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.expanded-content {
|
|
141
|
+
background-color: var(--surface-container);
|
|
142
|
+
}
|
|
143
|
+
</style>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const { title, content, link, icon } = defineProps({
|
|
3
|
+
title: String,
|
|
4
|
+
content: String,
|
|
5
|
+
link: {
|
|
6
|
+
/** @type {import('vue').PropType<{ text:string ,href: string,target?: string }>} */
|
|
7
|
+
type: Object,
|
|
8
|
+
required: false,
|
|
9
|
+
},
|
|
10
|
+
icon: {
|
|
11
|
+
/** @type {import('vue').PropType<{ html: string, width?: number, height?: number }>} */
|
|
12
|
+
type: Object,
|
|
13
|
+
required: false,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
const iconStyle = {
|
|
17
|
+
width: (icon?.width ?? 40) + "px",
|
|
18
|
+
height: (icon?.height ?? 40) + "px",
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
<template>
|
|
22
|
+
<article class="surface-container-lowest vertical large-padding">
|
|
23
|
+
<div>
|
|
24
|
+
<div v-if="icon" :style="iconStyle" v-html="icon.html" class="icon"></div>
|
|
25
|
+
<h5 class="small">{{ title }}</h5>
|
|
26
|
+
<p>
|
|
27
|
+
<slot>{{ content }}</slot>
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
<nav>
|
|
31
|
+
<a
|
|
32
|
+
v-if="link"
|
|
33
|
+
:href="link.href"
|
|
34
|
+
:target="link.target ?? '_blank'"
|
|
35
|
+
class="button transparent bold primary-text no-padding"
|
|
36
|
+
>
|
|
37
|
+
<span>
|
|
38
|
+
{{ link.text }}
|
|
39
|
+
</span>
|
|
40
|
+
<i class="mdi mdi-chevron-right arrow"></i>
|
|
41
|
+
</a>
|
|
42
|
+
</nav>
|
|
43
|
+
</article>
|
|
44
|
+
</template>
|
|
45
|
+
<style scoped>
|
|
46
|
+
/**
|
|
47
|
+
* Undo Vitepress missing with @eox/ui styles
|
|
48
|
+
*/
|
|
49
|
+
.VPHome .vp-doc a.button.bold {
|
|
50
|
+
font-weight: bold;
|
|
51
|
+
}
|
|
52
|
+
.VPHome .vp-doc h5.small {
|
|
53
|
+
font-size: 1.25rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Custom styles
|
|
58
|
+
*/
|
|
59
|
+
article {
|
|
60
|
+
justify-content: space-between;
|
|
61
|
+
transition: all 0.3s ease-in-out;
|
|
62
|
+
}
|
|
63
|
+
.arrow {
|
|
64
|
+
transition: transform 0.2s;
|
|
65
|
+
}
|
|
66
|
+
.button:hover .arrow {
|
|
67
|
+
transform: translateX(4px);
|
|
68
|
+
}
|
|
69
|
+
</style>
|
|
@@ -1,65 +1,91 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useData, withBase } from "vitepress";
|
|
3
3
|
import { data as features } from "../features.data.js";
|
|
4
|
+
import FeatureCard from "./FeatureCard.vue";
|
|
5
|
+
|
|
6
|
+
const { cards, sectionTitle, background } = defineProps({
|
|
7
|
+
sectionTitle: {
|
|
8
|
+
type: String,
|
|
9
|
+
},
|
|
10
|
+
cards: {
|
|
11
|
+
/**
|
|
12
|
+
* @type {import('vue').PropType<Array<{
|
|
13
|
+
id: number | string,
|
|
14
|
+
title: string,
|
|
15
|
+
content: string,
|
|
16
|
+
link?: { text: string, href: string,target?: string },
|
|
17
|
+
icon?: {
|
|
18
|
+
html: string,
|
|
19
|
+
width?: number,
|
|
20
|
+
height?: number,
|
|
21
|
+
}
|
|
22
|
+
}>>}
|
|
23
|
+
*/
|
|
24
|
+
type: Array,
|
|
25
|
+
},
|
|
26
|
+
background: {
|
|
27
|
+
type: String,
|
|
28
|
+
default: "primary primary-gradient-bg",
|
|
29
|
+
},
|
|
30
|
+
});
|
|
4
31
|
|
|
5
32
|
const { page, site } = useData();
|
|
6
33
|
|
|
7
|
-
const featuresExcerpts =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
const featuresExcerpts =
|
|
35
|
+
cards && cards.length
|
|
36
|
+
? cards
|
|
37
|
+
: features.map((f) => {
|
|
38
|
+
if (import.meta.env.SSR) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const el = document.createElement("html");
|
|
42
|
+
el.innerHTML = f.html;
|
|
43
|
+
/** @type {HTMLElement} */
|
|
44
|
+
const featureSection = el.querySelector("featuresection");
|
|
45
|
+
return featureSection &&
|
|
46
|
+
!page.value.relativePath.includes(f.url.replace("/", ""))
|
|
47
|
+
? {
|
|
48
|
+
id: Symbol(),
|
|
49
|
+
title: featureSection.title,
|
|
50
|
+
content: featureSection.textContent,
|
|
51
|
+
icon: featureSection.getAttribute("image")
|
|
52
|
+
? {
|
|
53
|
+
html: `<img src="${withBase(featureSection.getAttribute("image"))}" alt="Feature icon" />`,
|
|
54
|
+
width: 64,
|
|
55
|
+
height: 64,
|
|
56
|
+
}
|
|
57
|
+
: undefined,
|
|
58
|
+
link: {
|
|
59
|
+
href: f.url,
|
|
60
|
+
text: "Read more",
|
|
61
|
+
target: "_self",
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
: [];
|
|
65
|
+
});
|
|
66
|
+
const siteTitle = sectionTitle || `More ${site.value.title} features:`;
|
|
24
67
|
</script>
|
|
25
68
|
|
|
26
69
|
<template>
|
|
27
|
-
<section class="
|
|
70
|
+
<section :class="`${background} full-width`">
|
|
28
71
|
<div class="large-space"></div>
|
|
29
72
|
<div class="large-space"></div>
|
|
30
73
|
<div class="holder large-padding">
|
|
31
|
-
<h5>
|
|
74
|
+
<h5>{{ siteTitle }}</h5>
|
|
32
75
|
<div class="medium-space"></div>
|
|
33
|
-
<div class="
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
/>
|
|
47
|
-
<div class="max">
|
|
48
|
-
<h6 class="small">
|
|
49
|
-
<strong>{{ feature.title }}</strong>
|
|
50
|
-
</h6>
|
|
51
|
-
<p>{{ feature.description }}</p>
|
|
52
|
-
<div class="max"></div>
|
|
53
|
-
<nav>
|
|
54
|
-
<a class="link" :href="withBase(feature.link)">
|
|
55
|
-
<span>Read more</span>
|
|
56
|
-
</a>
|
|
57
|
-
</nav>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
</article>
|
|
61
|
-
</a>
|
|
62
|
-
</ClientOnly>
|
|
76
|
+
<div class="cards-gallery">
|
|
77
|
+
<div
|
|
78
|
+
v-for="feature in featuresExcerpts"
|
|
79
|
+
:key="feature.id"
|
|
80
|
+
class="card-wrapper"
|
|
81
|
+
>
|
|
82
|
+
<FeatureCard
|
|
83
|
+
:title="feature.title"
|
|
84
|
+
:content="feature.content"
|
|
85
|
+
:icon="feature.icon"
|
|
86
|
+
:link="feature.link"
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
63
89
|
</div>
|
|
64
90
|
</div>
|
|
65
91
|
<div class="large-space"></div>
|
|
@@ -68,11 +94,33 @@ const featuresExcerpts = features.map((f) => {
|
|
|
68
94
|
</template>
|
|
69
95
|
|
|
70
96
|
<style scoped>
|
|
71
|
-
.
|
|
72
|
-
.grid > a > article {
|
|
73
|
-
height: 100%;
|
|
97
|
+
.cards-gallery {
|
|
74
98
|
display: flex;
|
|
75
|
-
flex-
|
|
76
|
-
|
|
99
|
+
flex-wrap: wrap;
|
|
100
|
+
gap: 24px;
|
|
101
|
+
margin-top: 16px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.card-wrapper {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex: 0 0 calc(25% - 18px);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@media (max-width: 1200px) {
|
|
110
|
+
.card-wrapper {
|
|
111
|
+
flex: 0 0 calc(33.333% - 16px);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
@media (max-width: 960px) {
|
|
116
|
+
.card-wrapper {
|
|
117
|
+
flex: 0 0 calc(50% - 12px);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@media (max-width: 640px) {
|
|
122
|
+
.card-wrapper {
|
|
123
|
+
flex: 0 0 100%;
|
|
124
|
+
}
|
|
77
125
|
}
|
|
78
126
|
</style>
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import NewsBanner from "./components/NewsBanner.vue";
|
|
|
11
11
|
import CookieBanner from "./components/CookieBanner.vue";
|
|
12
12
|
import CookieSettings from "./components/CookieSettings.vue";
|
|
13
13
|
import NotFound from "./components/NotFound.vue";
|
|
14
|
+
import DataTable from "./components/DataTable.vue";
|
|
14
15
|
import Layout from "./Layout.vue";
|
|
15
16
|
import "./style.css";
|
|
16
17
|
|
|
@@ -30,6 +31,7 @@ export default {
|
|
|
30
31
|
app.component("CookieBanner", CookieBanner);
|
|
31
32
|
app.component("CookieSettings", CookieSettings);
|
|
32
33
|
app.component("NotFound", NotFound);
|
|
34
|
+
app.component("DataTable", DataTable);
|
|
33
35
|
|
|
34
36
|
router.onAfterRouteChanged = () => {
|
|
35
37
|
if (!import.meta.env.SSR) {
|