@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 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(`https://hub-brands.eox.at/${brandId}/config.mjs`);
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.10.1",
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.4",
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 = features.map((f) => {
8
- if (import.meta.env.SSR) {
9
- return false;
10
- }
11
- const el = document.createElement("html");
12
- el.innerHTML = f.html;
13
- const featureSection = el.querySelector("featuresection");
14
- return featureSection &&
15
- !page.value.relativePath.includes(f.url.replace("/", ""))
16
- ? {
17
- title: featureSection.title,
18
- description: featureSection.textContent,
19
- image: featureSection.getAttribute("image"),
20
- link: f.url,
21
- }
22
- : false;
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="primary primary-gradient-bg full-width">
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>More {{ site.title }} features:</h5>
74
+ <h5>{{ siteTitle }}</h5>
32
75
  <div class="medium-space"></div>
33
- <div class="grid">
34
- <ClientOnly>
35
- <a
36
- v-for="(feature, index) in featuresExcerpts.filter((f) => f)"
37
- class="feature s12 m4 wave"
38
- :href="withBase(feature.link)"
39
- >
40
- <article class="large-padding">
41
- <div class="row top-align">
42
- <img
43
- v-if="feature.image"
44
- class="round extra"
45
- :src="withBase(feature.image)"
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
- .grid > a,
72
- .grid > a > article {
73
- height: 100%;
97
+ .cards-gallery {
74
98
  display: flex;
75
- flex-direction: column;
76
- justify-content: space-between;
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) {