@eox/pages-theme-eox 1.0.0 → 1.1.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.
@@ -36,7 +36,7 @@
36
36
  <li v-for="item in theme.nav.filter((item) => !item.action)">
37
37
  <details v-if="item.items">
38
38
  <summary>
39
- <span class="max">{{ item.text }}</span>
39
+ <span class="max">{{ t(item.text, theme.i18n) }}</span>
40
40
  <i class="mdi mdi-chevron-down"></i>
41
41
  </summary>
42
42
  <ul class="list">
@@ -51,9 +51,31 @@
51
51
  :rel="item.rel"
52
52
  :class="item.action ? 'button large medium-elevate cta' : ''"
53
53
  >
54
- <span>{{ item.text }}</span>
54
+ <span>{{ t(item.text, theme.i18n) }}</span>
55
55
  </a>
56
56
  </li>
57
+ <li v-if="langs && langs.length > 1">
58
+ <details>
59
+ <summary>
60
+ <i class="mdi mdi-translate small"></i>
61
+ <span class="max">{{
62
+ langs.find((l) => l.active)?.label
63
+ }}</span>
64
+ <i class="mdi mdi-chevron-down small"></i>
65
+ </summary>
66
+ <ul class="list">
67
+ <li
68
+ v-for="lang in langs.filter((l) => !l.active)"
69
+ :key="lang.link"
70
+ :class="{ active: lang.active }"
71
+ >
72
+ <a :href="withBase(lang.link)" data-ui="#mobile-menu">
73
+ <span>{{ lang.label }}</span>
74
+ </a>
75
+ </li>
76
+ </ul>
77
+ </details>
78
+ </li>
57
79
  </ul>
58
80
  <div class="grid">
59
81
  <div class="s12">
@@ -69,7 +91,7 @@
69
91
  : 'primary-text'
70
92
  "
71
93
  >
72
- <span>{{ item.text }}</span>
94
+ <span>{{ t(item.text, theme.i18n) }}</span>
73
95
  <i
74
96
  v-if="
75
97
  item.action === 'primary' || item.action === 'secondary'
@@ -119,7 +141,7 @@
119
141
  class="button text"
120
142
  :class="{ active: isActive(item, route.path) }"
121
143
  >
122
- <span>{{ item.text }}</span>
144
+ <span>{{ t(item.text, theme.i18n) }}</span>
123
145
  <i class="mdi mdi-chevron-down"></i>
124
146
  <menu class="no-wrap surface-container-lowest">
125
147
  <NavDropdown :items="item.items" />
@@ -132,7 +154,7 @@
132
154
  :href="withBase(item.link)"
133
155
  :target="item.target"
134
156
  :rel="item.rel"
135
- >{{ item.text }}</a
157
+ >{{ t(item.text, theme.i18n) }}</a
136
158
  >
137
159
  </li>
138
160
  </ul>
@@ -153,7 +175,7 @@
153
175
  :rel="item.rel"
154
176
  @click="trackEvent(['CTA', 'Click', 'Nav', item.text])"
155
177
  >
156
- <span>{{ item.text }}</span>
178
+ <span>{{ t(item.text, theme.i18n) }}</span>
157
179
  <i
158
180
  v-if="item.action === 'primary' || item.action === 'secondary'"
159
181
  class="mdi mdi-arrow-right"
@@ -173,20 +195,79 @@
173
195
  </a>
174
196
  </li>
175
197
  </ul>
198
+ <ul class="left-align no-margin" v-if="langs && langs.length > 1">
199
+ <li>
200
+ <button class="button text">
201
+ <i class="mdi mdi-translate small"></i>
202
+ <i class="mdi mdi-chevron-down small"></i>
203
+ <menu class="no-wrap surface-container-lowest">
204
+ <li
205
+ v-for="lang in langs"
206
+ :key="lang.link"
207
+ :class="{ active: lang.active }"
208
+ >
209
+ <a :href="withBase(lang.link)">
210
+ <span>{{ lang.label }}</span>
211
+ </a>
212
+ </li>
213
+ </menu>
214
+ </button>
215
+ </li>
216
+ </ul>
176
217
  </nav>
177
218
  </nav>
178
219
  </div>
179
220
  </template>
180
221
 
181
222
  <script setup>
223
+ import { computed } from "vue";
182
224
  import { useData, useRoute, withBase } from "vitepress";
183
- import { trackEvent, isActive } from "../helpers";
225
+ import { t, trackEvent, isActive } from "../helpers";
184
226
  import NavDropdown from "./NavDropdown.vue";
185
227
  import MobileNavDropdown from "./MobileNavDropdown.vue";
186
228
 
187
- const { site, theme } = useData();
229
+ const { site, theme, localeIndex } = useData();
188
230
  const route = useRoute();
189
231
 
232
+ const langs = computed(() => {
233
+ const locales = site.value.locales;
234
+ if (!locales || Object.keys(locales).length <= 1) {
235
+ return [];
236
+ }
237
+ return Object.entries(locales).map(([id, locale]) => {
238
+ const currentPath = route.path;
239
+ const base = withBase("");
240
+ let pathRelativeToBase = currentPath;
241
+ if (base !== "/" && currentPath.startsWith(base)) {
242
+ pathRelativeToBase = currentPath.substring(base.length);
243
+ }
244
+
245
+ const currentLocaleId = localeIndex.value;
246
+ const currentLocalePrefix =
247
+ currentLocaleId === "root" ? "" : `/${currentLocaleId}`;
248
+
249
+ let pathWithoutLocale = pathRelativeToBase;
250
+ if (
251
+ currentLocalePrefix &&
252
+ pathRelativeToBase.startsWith(currentLocalePrefix)
253
+ ) {
254
+ pathWithoutLocale = pathRelativeToBase.substring(
255
+ currentLocalePrefix.length,
256
+ );
257
+ }
258
+
259
+ const targetLocalePrefix = id === "root" ? "" : `/${id}`;
260
+ let link = `${targetLocalePrefix}${pathWithoutLocale}`.replace(/\/+/g, "/");
261
+ if (!link) link = "/";
262
+
263
+ return {
264
+ label: locale.label,
265
+ link: link,
266
+ active: id === currentLocaleId,
267
+ };
268
+ });
269
+ });
270
+
190
271
  if (!import.meta.env.SSR) {
191
272
  const scrollListener = () => {
192
273
  const nav = document.querySelector(".top-nav");
@@ -289,4 +370,7 @@ nav.nav-desktop {
289
370
  display: flex;
290
371
  }
291
372
  }
373
+ details summary i.mdi-translate {
374
+ transform: rotate(0deg) !important;
375
+ }
292
376
  </style>
@@ -9,7 +9,7 @@
9
9
  class="row max padding"
10
10
  :class="{ active: isActive(item, route.path) }"
11
11
  >
12
- <span class="max">{{ item.text }}</span>
12
+ <span class="max">{{ t(item.text, theme.i18n) }}</span>
13
13
  <i class="mdi mdi-chevron-right"></i>
14
14
  </a>
15
15
  <a
@@ -17,7 +17,7 @@
17
17
  class="row max padding"
18
18
  :class="{ active: isActive(item, route.path) }"
19
19
  >
20
- <span class="max">{{ item.text }}</span>
20
+ <span class="max">{{ t(item.text, theme.i18n) }}</span>
21
21
  <i class="mdi mdi-chevron-right"></i>
22
22
  </a>
23
23
  <menu class="no-wrap surface-container-lowest">
@@ -34,21 +34,21 @@
34
34
  class="row"
35
35
  :class="{ active: isActive(item, route.path) }"
36
36
  >
37
- <span>{{ item.text }}</span>
37
+ <span>{{ t(item.text, theme.i18n) }}</span>
38
38
  </a>
39
39
  <span
40
40
  v-else
41
41
  class="row"
42
42
  :class="{ active: isActive(item, route.path) }"
43
- >{{ item.text }}</span
43
+ >{{ t(item.text, theme.i18n) }}</span
44
44
  >
45
45
  </li>
46
46
  </template>
47
47
  </template>
48
48
 
49
49
  <script setup>
50
- import { withBase, useRoute } from "vitepress";
51
- import { isActive } from "../helpers";
50
+ import { withBase, useRoute, useData } from "vitepress";
51
+ import { isActive, t } from "../helpers";
52
52
 
53
53
  defineProps({
54
54
  items: {
@@ -58,4 +58,5 @@ defineProps({
58
58
  });
59
59
 
60
60
  const route = useRoute();
61
+ const { theme } = useData();
61
62
  </script>
@@ -1,14 +1,20 @@
1
1
  <template>
2
2
  <div class="VPPage">
3
3
  <h1>404</h1>
4
- <p>The page you requested was not found.</p>
4
+ <p>{{ t("The page you requested was not found.", theme.i18n) }}</p>
5
5
  <div class="small-space"></div>
6
6
  <nav>
7
7
  <a class="button responsive-mobile" href="/">
8
8
  <i class="mdi mdi-arrow-left"></i>
9
- <span>Back to home</span>
9
+ <span>{{ t("Back to home", theme.i18n) }}</span>
10
10
  </a>
11
11
  </nav>
12
12
  <div class="large-space"></div>
13
13
  </div>
14
14
  </template>
15
+
16
+ <script setup>
17
+ import { useData } from "vitepress";
18
+ import { t } from "../helpers";
19
+ const { theme } = useData();
20
+ </script>
@@ -48,7 +48,7 @@
48
48
  :href="addPlanConfig(contactLink, plan)"
49
49
  class="button responsive bold margin-top-1 margin-bottom-2"
50
50
  >
51
- Contact us
51
+ {{ t("Contact us", theme.i18n) }}
52
52
  </a>
53
53
  </ClientOnly>
54
54
  </div>
@@ -58,7 +58,9 @@
58
58
  <!-- Main Plans Table -->
59
59
  <div class="wrapper" :style="gridStyle">
60
60
  <div class="cell orig-col-1 l top-margin">
61
- <h6 v-if="!localDetails.length" class="bold small">Plans:</h6>
61
+ <h6 v-if="!localDetails.length" class="bold small">
62
+ {{ t("Plans:", theme.i18n) }}
63
+ </h6>
62
64
  </div>
63
65
  <div
64
66
  v-for="(plan, index) in localPlans"
@@ -69,7 +71,9 @@
69
71
  <h6 class="primary-text bold top-margin">{{ plan.name }}</h6>
70
72
  </div>
71
73
 
72
- <div class="cell orig-col-1 l">Price (per month):</div>
74
+ <div class="cell orig-col-1 l">
75
+ {{ t("Price (per month):", theme.i18n) }}
76
+ </div>
73
77
  <div
74
78
  v-for="(plan, index) in localPlans"
75
79
  :key="'price-' + index"
@@ -142,7 +146,7 @@
142
146
  :class="`surface-container-low cell bottom-cell orig-col-${index + 2} center-align`"
143
147
  >
144
148
  <a v-if="plan.link" :href="plan.link"
145
- >See all features
149
+ >{{ t("See all features", theme.i18n) }}
146
150
  <svg
147
151
  style="width: 16px; height: 16px"
148
152
  xmlns="http://www.w3.org/2000/svg"
@@ -168,7 +172,7 @@
168
172
  v-if="showSales"
169
173
  :href="addPlanConfig(contactLink, plan)"
170
174
  class="button responsive bold"
171
- >Contact us</a
175
+ >{{ t("Contact us", theme.i18n) }}</a
172
176
  >
173
177
  </ClientOnly>
174
178
  </div>
@@ -185,7 +189,7 @@
185
189
 
186
190
  <div class="wrapper" :style="secondaryGridStyle">
187
191
  <div class="cell cell orig-col-1 l">
188
- <div class="">Additional price:</div>
192
+ <div class="">{{ t("Additional price:", theme.i18n) }}</div>
189
193
  </div>
190
194
  <div
191
195
  v-for="(plan, detailPlanIndex) in detail.plans"
@@ -231,15 +235,28 @@
231
235
  </template>
232
236
 
233
237
  <p class="small grey-text m-4" v-if="showVAT">
234
- * All prices are given excluding VAT. Prices are valid until
238
+ *
239
+ {{
240
+ t(
241
+ "All prices are given excluding VAT. Prices are valid until",
242
+ theme.i18n,
243
+ )
244
+ }}
235
245
  {{ showVAT }}.
236
246
  </p>
237
247
  </div>
238
248
  </template>
239
249
 
240
250
  <script>
251
+ import { t } from "../helpers.js";
252
+ import { useData } from "vitepress";
253
+
241
254
  export default {
242
255
  name: "PricingTable",
256
+ setup() {
257
+ const { theme } = useData();
258
+ return { theme, t };
259
+ },
243
260
  props: {
244
261
  config: {
245
262
  type: Object,
@@ -0,0 +1,85 @@
1
+ import Tutorial from "./Tutorial.vue";
2
+
3
+ describe("<Tutorial />", () => {
4
+ it("renders with all slots provided", () => {
5
+ cy.mount(Tutorial, {
6
+ slots: {
7
+ default: "<p>Learn how to configure the map component.</p>",
8
+ demo: "<div class='demo-content'>Interactive Map Demo</div>",
9
+ code: "<pre><code>const map = new Map();</code></pre>",
10
+ controls: "<button>Reset</button>",
11
+ },
12
+ });
13
+
14
+ cy.get(".description").should(
15
+ "contain.text",
16
+ "Learn how to configure the map component.",
17
+ );
18
+ cy.get(".demo-wrapper").should("exist");
19
+ cy.get(".demo-content").should("contain.text", "Interactive Map Demo");
20
+ cy.get(".code-view code").should("contain.text", "const map = new Map()");
21
+ cy.get("button").should("contain.text", "Reset");
22
+ });
23
+
24
+ it("supports both default and named description slot", () => {
25
+ cy.mount(Tutorial, {
26
+ slots: {
27
+ description: "<p>Using named slot</p>",
28
+ demo: "<div>Demo</div>",
29
+ code: "<pre>Code</pre>",
30
+ },
31
+ });
32
+
33
+ cy.get(".description").should("contain.text", "Using named slot");
34
+ });
35
+
36
+ it("renders with only required slots", () => {
37
+ cy.mount(Tutorial, {
38
+ slots: {
39
+ demo: "<div>Demo Only</div>",
40
+ code: "<pre><code>console.log('test');</code></pre>",
41
+ },
42
+ });
43
+
44
+ cy.get(".description").should("not.exist");
45
+ cy.get(".demo-wrapper").should("exist");
46
+ cy.get(".code-view").should("exist");
47
+ });
48
+
49
+ it("applies custom demoHeight prop", () => {
50
+ cy.mount(Tutorial, {
51
+ props: {
52
+ demoHeight: "700px",
53
+ },
54
+ slots: {
55
+ demo: "<div>Tall Demo</div>",
56
+ code: "<pre>Code</pre>",
57
+ },
58
+ });
59
+
60
+ cy.get(".demo-wrapper").should("have.css", "height", "700px");
61
+ });
62
+
63
+ it("renders with code examples correctly", () => {
64
+ cy.mount(Tutorial, {
65
+ slots: {
66
+ default: "<p>Map configuration example</p>",
67
+ demo: "<div>Map Component</div>",
68
+ code: `<pre><code>import "@eox/map";
69
+
70
+ const config = {
71
+ center: [0, 0],
72
+ zoom: 5
73
+ };
74
+
75
+ const map = document.querySelector("eox-map");
76
+ map.setConfig(config);</code></pre>`,
77
+ },
78
+ });
79
+
80
+ cy.get(".code-view code")
81
+ .should("contain.text", 'import "@eox/map"')
82
+ .and("contain.text", "const config =")
83
+ .and("contain.text", "map.setConfig(config)");
84
+ });
85
+ });
@@ -0,0 +1,162 @@
1
+ <template>
2
+ <article class="tutorial card border no-overflow">
3
+ <!-- Demo Section -->
4
+ <div>
5
+ <!-- Description Section -->
6
+ <div
7
+ v-if="$slots.default || $slots.description"
8
+ class="description padding border-bottom"
9
+ >
10
+ <slot name="description">
11
+ <slot></slot>
12
+ </slot>
13
+ </div>
14
+
15
+ <ClientOnly>
16
+ <div
17
+ class="demo-wrapper relative border-bottom"
18
+ :style="{ height: demoHeight }"
19
+ v-if="$slots.demo"
20
+ >
21
+ <slot name="demo"></slot>
22
+ </div>
23
+ </ClientOnly>
24
+ </div>
25
+
26
+ <div v-if="$slots.controls" class="padding">
27
+ <slot name="controls"></slot>
28
+ </div>
29
+
30
+ <!-- Code Section -->
31
+ <div class="code-view top-margin">
32
+ <div v-show="!collapsible || isCodeExpanded">
33
+ <slot name="code"></slot>
34
+ </div>
35
+ </div>
36
+ </article>
37
+ </template>
38
+
39
+ <script setup>
40
+ /**
41
+ * Tutorial component for displaying interactive demos with code examples.
42
+ *
43
+ * @component
44
+ * @example
45
+ * <Tutorial demoHeight="600px">
46
+ * <p>This tutorial shows how to use the map component.</p>
47
+ *
48
+ * <template #demo>
49
+ * <eox-map></eox-map>
50
+ * </template>
51
+ *
52
+ * <template #code>
53
+ *
54
+ * ::: code-group
55
+ * ```vue
56
+ * <template>
57
+ * first tab content
58
+ * </template>
59
+ * ```
60
+ * ```html
61
+ * <html>
62
+ * auto highlighted code content
63
+ * </html>
64
+ * ```
65
+ * ```js
66
+ * console.log("third tab content");
67
+ * ```
68
+ * :::
69
+ * </template>
70
+
71
+ * <template #controls>
72
+ * <button>Reset Map</button>
73
+ * </template>
74
+ * </Tutorial>
75
+ */
76
+
77
+ defineProps({
78
+ demoHeight: {
79
+ type: String,
80
+ default: "500px",
81
+ },
82
+ });
83
+ </script>
84
+
85
+ <style scoped>
86
+ .demo-wrapper {
87
+ overflow: hidden;
88
+ transition: height 0.3s ease;
89
+ }
90
+
91
+ /* Code and pre styling - VitePress compatibility */
92
+ :deep(code *),
93
+ :deep(pre *),
94
+ :deep(.vp-code *),
95
+ :deep(.vp-code-group *:not(.tabs *)),
96
+ :deep(.url-display),
97
+ :deep(kbd),
98
+ :deep(samp),
99
+ :deep(var),
100
+ :deep(.monospace) {
101
+ font-family: "SFMono-Regular", "Menlo", "Monaco", "Consolas",
102
+ "Liberation Mono", "Ubuntu Mono", "Courier New", monospace !important;
103
+ }
104
+
105
+ /* Inline code */
106
+ :deep(code) {
107
+ background: #f6f8fa;
108
+ color: #24292e;
109
+ padding: 2px 4px;
110
+ border-radius: 16px;
111
+ }
112
+
113
+ /* Code blocks */
114
+ :deep(pre) {
115
+ background: #f6f8fa;
116
+ color: #24292e;
117
+ padding: 16px;
118
+ border-radius: 4px;
119
+ overflow-x: auto;
120
+ line-height: 1.4;
121
+ margin: 16px 0;
122
+ border: 1px solid #e1e4e8;
123
+ }
124
+
125
+ /* VitePress code groups */
126
+ :deep(.vp-code-group .tabs) {
127
+ padding: 0;
128
+ margin-left: 0;
129
+ margin-right: 0;
130
+ }
131
+
132
+ :deep(.vp-code-group .tabs label) {
133
+ flex: 1;
134
+ text-align: center;
135
+ }
136
+
137
+ :deep(.vp-code-group .tabs input:checked + label) {
138
+ color: var(--primary);
139
+ }
140
+
141
+ :deep(.vp-code-group .tabs label:hover) {
142
+ background: var(--surface-container-high);
143
+ }
144
+
145
+ :deep(.vp-code-group .tabs label::after) {
146
+ left: 0;
147
+ right: 0;
148
+ height: 1px;
149
+ }
150
+ :deep(.vp-code-group input:checked + label::after) {
151
+ height: 2px;
152
+ }
153
+
154
+ :deep(.vp-code-group .blocks) {
155
+ margin-top: 0.5rem;
156
+ }
157
+
158
+ :deep(.code-view div[class*="language-"]) {
159
+ margin-left: 0;
160
+ margin-right: 0;
161
+ }
162
+ </style>
package/src/helpers.js CHANGED
@@ -173,3 +173,28 @@ export const isActive = (item, path) => {
173
173
 
174
174
  return false;
175
175
  };
176
+
177
+ /**
178
+ * Get the translation for a given key from an i18n object.
179
+ * Falls back to the key if no translation is found.
180
+ * Supports dot notation for nested objects.
181
+ *
182
+ * @param {string} key - The key to translate
183
+ * @param {object} i18n - The translation table
184
+ * @returns {string} The translated string or the key itself
185
+ */
186
+ export const t = (key, i18n) => {
187
+ if (!key) return "";
188
+ if (!i18n) return key;
189
+
190
+ let obj = i18n;
191
+ const parts = key.split(".");
192
+ for (const part of parts) {
193
+ if (obj && typeof obj === "object" && part in obj) {
194
+ obj = obj[part];
195
+ } else {
196
+ return key;
197
+ }
198
+ }
199
+ return typeof obj === "string" ? obj : key;
200
+ };
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ import CookieBanner from "./components/CookieBanner.vue";
12
12
  import CookieSettings from "./components/CookieSettings.vue";
13
13
  import NotFound from "./components/NotFound.vue";
14
14
  import DataTable from "./components/DataTable.vue";
15
+ import Tutorial from "./components/Tutorial.vue";
15
16
  import Layout from "./Layout.vue";
16
17
  import "./style.css";
17
18
 
@@ -32,6 +33,7 @@ export default {
32
33
  app.component("CookieSettings", CookieSettings);
33
34
  app.component("NotFound", NotFound);
34
35
  app.component("DataTable", DataTable);
36
+ app.component("Tutorial", Tutorial);
35
37
 
36
38
  router.onAfterRouteChanged = () => {
37
39
  if (!import.meta.env.SSR) {