@eox/pages-theme-eox 0.4.17 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eox/pages-theme-eox",
3
- "version": "0.4.17",
3
+ "version": "0.5.1",
4
4
  "type": "module",
5
5
  "description": "Vitepress Theme with EOX branding",
6
6
  "main": "src/index.js",
package/src/Layout.vue CHANGED
@@ -1,6 +1,5 @@
1
1
  <template>
2
2
  <Layout :class="`layout-${frontmatter.layout}`">
3
- <!-- <main class="responsive"> -->
4
3
  <template #layout-top>
5
4
  <slot name="layout-top"></slot>
6
5
  <div class="top-nav at-top row center-align">
@@ -27,7 +26,7 @@
27
26
  </a>
28
27
  <div class="max"></div>
29
28
  <button data-ui="#mobile-menu" class="circle transparent">
30
- <i class="mdi mdi-close primary-text"></i>
29
+ <i class="mdi mdi-close"></i>
31
30
  </button>
32
31
  </nav>
33
32
  <ul class="list border large-space">
@@ -37,14 +36,9 @@
37
36
  :href="withBase(item.link)"
38
37
  :target="item.target"
39
38
  :rel="item.rel"
40
- :class="
41
- item.action
42
- ? 'button large medium-elevate cta'
43
- : 'primary-text'
44
- "
39
+ :class="item.action ? 'button large medium-elevate cta' : ''"
45
40
  >
46
41
  <span>{{ item.text }}</span>
47
- <i v-if="item.action" class="mdi mdi-arrow-right"></i>
48
42
  </a>
49
43
  </li>
50
44
  </ul>
@@ -58,12 +52,17 @@
58
52
  :rel="item.rel"
59
53
  :class="
60
54
  item.action
61
- ? `button responsive large medium-elevate cta ${item.action}`
55
+ ? `button responsive large ${item.action === 'primary' || item.action === 'secondary' ? `${item.action} medium-elevate` : 'border no-elevate'}`
62
56
  : 'primary-text'
63
57
  "
64
58
  >
65
59
  <span>{{ item.text }}</span>
66
- <i v-if="item.action" class="mdi mdi-arrow-right"></i>
60
+ <i
61
+ v-if="
62
+ item.action === 'primary' || item.action === 'secondary'
63
+ "
64
+ class="mdi mdi-arrow-right"
65
+ ></i>
67
66
  </a>
68
67
  </nav>
69
68
  </div>
@@ -85,6 +84,7 @@
85
84
  <ul class="left-align no-margin">
86
85
  <li v-for="item in theme.nav.filter((i) => !i.action)">
87
86
  <a
87
+ class="button text"
88
88
  :href="withBase(item.link)"
89
89
  :target="item.target"
90
90
  :rel="item.rel"
@@ -94,50 +94,86 @@
94
94
  </ul>
95
95
  </nav>
96
96
  <div class="max"></div>
97
- <nav>
97
+ <nav class="actions">
98
98
  <ul class="left-align no-margin">
99
99
  <li v-for="item in theme.nav.filter((item) => item.action)">
100
100
  <a
101
101
  class="button right-align"
102
- :class="item.action === 'primary' ? '' : 'text'"
102
+ :class="
103
+ item.action === 'primary' || item.action === 'secondary'
104
+ ? item.action
105
+ : 'border'
106
+ "
103
107
  :href="withBase(item.link)"
104
108
  :target="item.target"
105
109
  :rel="item.rel"
106
110
  >
107
111
  <span>{{ item.text }}</span>
108
- <i class="mdi mdi-arrow-right"></i>
112
+ <i
113
+ v-if="
114
+ item.action === 'primary' || item.action === 'secondary'
115
+ "
116
+ class="mdi mdi-arrow-right"
117
+ ></i>
109
118
  </a>
110
119
  </li>
111
120
  </ul>
112
121
  </nav>
113
122
  </nav>
114
123
  </div>
115
- <header v-if="frontmatter.hero" class="primary center-align">
124
+ <header
125
+ v-if="frontmatter.hero"
126
+ class="primary primary-gradient-background"
127
+ >
128
+ <div
129
+ :class="`large-padding hero-container ${frontmatter.hero.image ? 'image' : ''}`"
130
+ >
131
+ <iframe
132
+ v-if="frontmatter.hero.image?.src?.includes('youtube')"
133
+ class="hero-image large-elevate small-round"
134
+ style="grid-area: image; aspect-ratio: 16/9"
135
+ :src="frontmatter.hero.image.src"
136
+ frameborder="0"
137
+ allowfullscreen
138
+ />
139
+ <img
140
+ v-else-if="frontmatter.hero.image"
141
+ class="hero-image large-elevate small-round"
142
+ style="grid-area: image"
143
+ :src="withBase(frontmatter.hero.image.src)"
144
+ />
145
+ <div class="title" style="grid-area: title; align-content: end">
146
+ <h1 class="bold" style="font-size: clamp(2rem, 5vw, 45px)">
147
+ {{ frontmatter.hero.text }}
148
+ </h1>
149
+ <p>{{ frontmatter.hero.tagline }}</p>
150
+ </div>
151
+ <div style="grid-area: actions">
152
+ <a
153
+ v-for="(action, i) in frontmatter.hero.actions"
154
+ :href="withBase(action.link)"
155
+ :target="action.target"
156
+ :rel="action.rel"
157
+ class="button extra small-margin"
158
+ :class="
159
+ action.theme === 'brand'
160
+ ? 'primary-text surface medium-elevate'
161
+ : action.theme === 'secondary'
162
+ ? 'secondary'
163
+ : 'border no-elevate white-text'
164
+ "
165
+ :style="i === 0 ? 'margin-left: 0 !important' : ''"
166
+ >
167
+ <span>{{ action.text }}</span>
168
+ <i v-if="action.theme !== 'alt'" class="mdi mdi-arrow-right"></i>
169
+ </a>
170
+ </div>
171
+ </div>
116
172
  <img
117
- v-if="frontmatter.hero.image"
173
+ v-if="frontmatter.hero.background"
118
174
  class="background-image"
119
- :src="withBase(frontmatter.hero.image.src)"
175
+ :src="withBase(frontmatter.hero.background.src)"
120
176
  />
121
- <h1
122
- class="bold medium-margin"
123
- style="font-size: clamp(2rem, 5vw, 45px)"
124
- >
125
- {{ frontmatter.hero.text }}
126
- </h1>
127
- <p>{{ frontmatter.hero.tagline }}</p>
128
- <div>
129
- <a
130
- v-for="action in frontmatter.hero.actions"
131
- :href="withBase(action.link)"
132
- :target="action.target"
133
- :rel="action.rel"
134
- class="button large medium-elevate cta"
135
- :class="action.theme"
136
- >
137
- <span>{{ action.text }}</span>
138
- <i class="mdi mdi-arrow-right"></i>
139
- </a>
140
- </div>
141
177
  </header>
142
178
  </template>
143
179
  <template #page-bottom>
@@ -146,41 +182,68 @@
146
182
  ></FeaturesGallery>
147
183
  </template>
148
184
  <template #layout-bottom>
149
- <footer class="surface row center-align">
150
- <div class="holder">
151
- <div class="">
152
- <nav class="padding right-align">
185
+ <footer class="surface-container-low row center-align">
186
+ <div class="holder large-padding">
187
+ <div class="large-space"></div>
188
+ <div class="grid">
189
+ <div class="s12 l6">
153
190
  <img
154
191
  :src="withBase(theme.logo)"
155
192
  :alt="`${site.title} logo`"
156
193
  class="logo"
157
194
  />
158
- <ul>
159
- <li v-for="item in theme.nav">
160
- <a
161
- :href="withBase(item.link)"
162
- :target="item.target"
163
- :rel="item.rel"
164
- >{{ item.text }}</a
165
- >
166
- </li>
167
- </ul>
168
- </nav>
195
+ <div class="small-space"></div>
196
+ <a
197
+ v-if="theme.nav.find((i) => i.link.includes('contact'))"
198
+ :href="theme.nav.find((i) => i.link.includes('contact')).link"
199
+ class="button small border no-margin"
200
+ style="color: var(--on-surface)"
201
+ >{{ theme.nav.find((i) => i.link.includes("contact")).text }}</a
202
+ >
203
+ <p v-html="theme.footer.copyright"></p>
204
+ </div>
205
+ <div class="s12 l6">
206
+ <div class="grid large-line">
207
+ <div class="s6">
208
+ <p class="bold">About</p>
209
+ <p v-for="item in theme.nav.filter((i) => !i.action)">
210
+ <a
211
+ :href="withBase(item.link)"
212
+ :target="item.target"
213
+ :rel="item.rel"
214
+ class="link"
215
+ >{{ item.text }}</a
216
+ >
217
+ </p>
218
+ </div>
219
+ <div class="s6">
220
+ <p class="bold">Legal</p>
221
+ <p>
222
+ <a
223
+ href="https://eox.at/impressum"
224
+ target="_blank"
225
+ class="link"
226
+ >About & Terms</a
227
+ >
228
+ </p>
229
+ <p>
230
+ <a
231
+ href="https://eox.at/privacy-notice"
232
+ target="_blank"
233
+ class="link"
234
+ >Privacy</a
235
+ >
236
+ </p>
237
+ </div>
238
+ </div>
239
+ </div>
169
240
  </div>
170
- <nav class="padding">
171
- <span v-html="theme.footer.copyright"></span>
172
- <a href="https://eox.at/impressum" target="_blank">About & Terms</a>
173
- <a href="https://eox.at/privacy-notice" target="_blank">Privacy</a>
174
- </nav>
241
+ <div class="large-space"></div>
175
242
  </div>
176
243
  </footer>
177
244
  <slot name="layout-bottom"></slot>
178
245
  </template>
179
- <!-- </main> -->
180
246
  </Layout>
181
- <!-- <main class="responsive">
182
-
183
- </main> -->
184
247
  </template>
185
248
 
186
249
  <script setup>
@@ -193,7 +256,7 @@ if (!import.meta.env.SSR) {
193
256
  const scrollListener = () => {
194
257
  const nav = document.querySelector(".top-nav");
195
258
 
196
- if (window.scrollY > window.innerHeight) {
259
+ if (window.scrollY > nav.clientHeight) {
197
260
  nav.classList.remove("at-top");
198
261
  } else {
199
262
  nav.classList.add("at-top");
@@ -206,10 +269,13 @@ if (!import.meta.env.SSR) {
206
269
  <style>
207
270
  .top-nav {
208
271
  position: sticky;
209
- z-index: 1;
272
+ z-index: 2;
210
273
  top: 0;
211
274
  background: var(--surface);
212
- box-shadow: var(--elevate2);
275
+ box-shadow:
276
+ 0 1px 5px #15142e0d,
277
+ 0 4px 16px #15142e1a,
278
+ inset 0 0 0 1px #ffffff80;
213
279
  display: flex;
214
280
  width: 100%;
215
281
  transition: all 0.3s ease-in-out;
@@ -217,19 +283,38 @@ if (!import.meta.env.SSR) {
217
283
  .top-nav.at-top {
218
284
  box-shadow: none;
219
285
  }
286
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.primary,
287
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.secondary {
288
+ display: none;
289
+ }
290
+ .Layout.layout-home .top-nav.at-top nav.actions {
291
+ gap: 0;
292
+ }
293
+ .top-nav:not(.at-top) > nav {
294
+ padding-top: 16px !important;
295
+ padding-bottom: 16px !important;
296
+ }
220
297
  .Layout.layout-home .top-nav.at-top {
221
298
  background: transparent;
222
299
  color: #f7f8f8;
223
300
  }
224
- .Layout.layout-home .top-nav.at-top nav .button {
225
- color: #00060a;
301
+ .Layout .top-nav nav .button {
302
+ color: var(--on-surface);
303
+ }
304
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button {
305
+ color: var(--on-surface);
226
306
  background: var(--surface);
227
307
  }
228
- .Layout.layout-home .top-nav.at-top nav .button.text {
308
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.text {
229
309
  color: #f7f8f8;
230
310
  background: none;
231
311
  }
232
- .Layout.layout-home .top-nav.at-top nav > a > .logo {
312
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.border {
313
+ color: #f7f8f8;
314
+ border-color: #f7f8f855;
315
+ background: none;
316
+ }
317
+ .Layout.layout-home .top-nav.at-top > nav > a > .logo {
233
318
  filter: brightness(0) invert(1);
234
319
  }
235
320
  img.logo {
@@ -251,14 +336,9 @@ nav.nav-desktop {
251
336
  }
252
337
  header {
253
338
  margin-top: calc(var(--vp-nav-height) * -1);
254
- background: radial-gradient(
255
- 74.48% 130.95% at 50% -12.27%,
256
- #0f9bff 0%,
257
- #004170 60%,
258
- #000c14 100%
259
- );
339
+ padding-top: 10rem !important;
340
+ padding-bottom: 10rem !important;
260
341
  height: 100svh;
261
- padding: 0 6rem !important;
262
342
  }
263
343
  @media (max-width: 1024px) {
264
344
  header {
@@ -276,41 +356,65 @@ header > img.background-image {
276
356
  left: 0;
277
357
  object-fit: cover;
278
358
  opacity: 0.4;
359
+ z-index: 0;
279
360
  }
280
- header > h1 {
361
+ header > .hero-container {
362
+ max-width: 1200px;
363
+ max-height: 80%;
364
+ margin-left: auto;
365
+ margin-right: auto;
366
+ display: grid;
367
+ grid-gap: 2rem;
368
+ grid-template-areas:
369
+ "title"
370
+ "actions"
371
+ "image";
372
+ grid-auto-rows: min-content;
373
+ text-align: center;
374
+ z-index: 1;
375
+ }
376
+ @media (min-width: 768px) {
377
+ header > .hero-container {
378
+ grid-template-areas:
379
+ "title"
380
+ "actions";
381
+ text-align: center;
382
+ }
383
+ header > .hero-container.image {
384
+ grid-template-areas:
385
+ "title image"
386
+ "actions image";
387
+ text-align: left;
388
+ grid-template-columns: repeat(2, minmax(0, 1fr));
389
+ }
390
+ }
391
+ header > .hero-container > .hero-image {
392
+ width: 100%;
393
+ object-fit: cover;
394
+ }
395
+ header > .hero-container > .title > h1 {
281
396
  margin-bottom: 24px;
282
397
  }
283
- header > p {
398
+ header > .hero-container > .title > p {
284
399
  font-size: 1.2rem;
285
- margin-bottom: 40px;
286
- }
287
- header .cta.brand {
288
- background-color: #f7f8f8;
289
- color: #00060a;
290
400
  }
291
- header .cta.alt {
292
- background-color: var(--primary, #002742);
293
- color: #f7f8f8;
401
+ header .cta:first-child {
402
+ margin-left: 0;
294
403
  }
295
- footer {
296
- padding: 0;
404
+ header .cta:last-child {
405
+ margin-right: 0;
297
406
  }
298
407
  .top-nav > nav,
299
408
  .top-nav > .holder,
300
- footer > .holder {
409
+ footer > .holder,
410
+ .full-width > .holder {
301
411
  width: 100%;
302
- max-width: 1400px;
303
- }
304
- footer nav {
305
- display: flex;
306
- flex-direction: column;
307
- }
308
- footer nav > ul {
309
- margin: 0;
412
+ max-width: 1200px;
413
+ margin-left: auto;
414
+ margin-right: auto;
310
415
  }
311
- @media (min-width: 768px) {
312
- footer nav {
313
- flex-direction: row;
314
- }
416
+ .top-nav > nav,
417
+ .top-nav > .holder {
418
+ max-width: 1400px;
315
419
  }
316
420
  </style>
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <section class="cta-section">
3
+ <div
4
+ class="center-align full-width"
5
+ :class="
6
+ dark !== undefined
7
+ ? 'primary primary-gradient-background'
8
+ : 'surface-container-low'
9
+ "
10
+ >
11
+ <div class="large-space"></div>
12
+ <div class="large-space"></div>
13
+ <div class="holder large-padding">
14
+ <h3 class="large bold">
15
+ {{ title }}
16
+ </h3>
17
+ <p class="large-text">{{ tagline }}</p>
18
+ <a
19
+ v-if="primaryButton"
20
+ class="button extra small-margin"
21
+ :class="dark !== undefined ? 'surface' : 'primary'"
22
+ :href="primaryLink"
23
+ >{{ primaryButton }} <i class="mdi mdi-arrow-right"></i
24
+ ></a>
25
+ <a
26
+ v-if="secondaryButton"
27
+ class="button extra small-margin secondary"
28
+ :href="secondaryLink"
29
+ >{{ secondaryButton }}<i class="mdi mdi-arrow-right"></i
30
+ ></a>
31
+ <a
32
+ v-if="altButton"
33
+ class="button border extra small-margin"
34
+ :class="dark !== undefined ? 'white-text' : ''"
35
+ :href="altLink"
36
+ >{{ altButton }}</a
37
+ >
38
+ </div>
39
+ <div class="large-space"></div>
40
+ <div class="large-space"></div>
41
+ <div class="large-space"></div>
42
+ </div>
43
+ </section>
44
+ </template>
45
+
46
+ <script setup>
47
+ const props = defineProps([
48
+ "dark",
49
+ "primaryButton",
50
+ "primaryLink",
51
+ "secondaryButton",
52
+ "secondaryLink",
53
+ "altButton",
54
+ "altLink",
55
+ "tagline",
56
+ "title",
57
+ ]);
58
+ </script>
@@ -1,21 +1,29 @@
1
1
  <template>
2
2
  <section
3
- :class="`feature-section full-width ${landing === undefined ? '' : 'landing'}`"
3
+ :class="`feature-section full-width ${landing === undefined ? '' : 'landing'} ${dark !== undefined ? 'dark' : ''} ${reverse !== undefined ? 'reverse' : ''}`"
4
4
  >
5
- <div class="holder">
5
+ <div
6
+ class="holder"
7
+ :class="dark !== undefined ? 'primary-gradient-background' : ''"
8
+ >
6
9
  <div class="text">
7
- <nav class="primary-text">
10
+ <nav class="">
8
11
  <i :class="`mdi ${icon}`"></i>
9
12
  {{ title }}
10
13
  </nav>
11
- <h5 :class="`title primary-text`">{{ tagline }}</h5>
14
+ <h5 v-if="landing !== undefined" class="title">{{ tagline }}</h5>
15
+ <h2 v-else class="title large">
16
+ <strong>{{ tagline }}</strong>
17
+ </h2>
12
18
  <p><slot></slot></p>
13
- <nav>
19
+ <div class="small-space"></div>
20
+ <div>
14
21
  <a
15
22
  v-if="primaryLink !== undefined"
16
23
  :href="withBase(primaryLink)"
17
24
  :target="primaryLink.includes('https://') ? '_blank' : '_self'"
18
- :class="`button primary medium-elevate`"
25
+ :class="`button primary medium-elevate no-margin`"
26
+ style="margin-right: 12px !important"
19
27
  >
20
28
  <span>{{ primaryButton || `Read more about ${title}` }}</span>
21
29
  <i class="mdi mdi-arrow-right"></i>
@@ -24,14 +32,28 @@
24
32
  v-if="secondaryLink !== undefined"
25
33
  :href="withBase(secondaryLink)"
26
34
  :target="secondaryLink.includes('https://') ? '_blank' : '_self'"
27
- class="button border"
35
+ class="button secondary medium-elevate no-margin"
36
+ style="color: var(--on-surface); margin-top: 12px !important"
28
37
  >
29
38
  <span>{{ secondaryButton || "Contact sales" }}</span>
30
39
  <i class="mdi mdi-arrow-right"></i>
31
40
  </a>
32
- </nav>
41
+ <a
42
+ v-if="altLink !== undefined"
43
+ :href="withBase(altLink)"
44
+ :target="altLink.includes('https://') ? '_blank' : '_self'"
45
+ class="button border no-margin"
46
+ style="color: var(--on-surface); margin-top: 12px !important"
47
+ >
48
+ <span>{{ altButton || "Contact sales" }}</span>
49
+ </a>
50
+ </div>
33
51
  </div>
34
- <img :src="withBase(image)" :alt="title" />
52
+ <img
53
+ :src="withBase(image)"
54
+ :alt="title"
55
+ class="small-round medium-elevate"
56
+ />
35
57
  </div>
36
58
  </section>
37
59
  </template>
@@ -39,13 +61,17 @@
39
61
  <script setup>
40
62
  import { withBase } from "vitepress";
41
63
  const props = defineProps([
64
+ "dark",
42
65
  "icon",
43
66
  "image",
44
67
  "landing",
45
68
  "primaryButton",
46
69
  "primaryLink",
70
+ "reverse",
47
71
  "secondaryButton",
48
72
  "secondaryLink",
73
+ "altButton",
74
+ "altLink",
49
75
  "tagline",
50
76
  "title",
51
77
  ]);
@@ -5,6 +5,9 @@ import { data as features } from "../features.data.js";
5
5
  const { page, site } = useData();
6
6
 
7
7
  const featuresExcerpts = features.map((f) => {
8
+ if (import.meta.env.SSR) {
9
+ return false;
10
+ }
8
11
  const el = document.createElement("html");
9
12
  el.innerHTML = f.html;
10
13
  const featureSection = el.querySelector("featuresection");
@@ -21,36 +24,54 @@ const featuresExcerpts = features.map((f) => {
21
24
  </script>
22
25
 
23
26
  <template>
24
- <h2>Read more about these other {{ site.title }} features:</h2>
25
- <div class="grid">
26
- <a
27
- v-for="(feature, index) in featuresExcerpts.filter((f) => f)"
28
- class="feature s12 m3"
29
- :href="withBase(feature.link)"
30
- >
31
- <article class="no-padding">
32
- <img
33
- v-if="feature.image"
34
- class="responsive small"
35
- :src="withBase(feature.image)"
36
- />
37
- <div class="padding">
38
- <h5 class="small">{{ feature.title }}</h5>
39
- <p>{{ feature.description }}</p>
40
- <nav>
41
- <a class="button" :href="withBase(feature.link)">
42
- <span>Read more</span>
43
- <i class="mdi mdi-arrow-right"></i>
44
- </a>
45
- </nav>
46
- </div>
47
- </article>
48
- </a>
27
+ <div class="large-space"></div>
28
+ <div class="primary primary-gradient-background full-width">
29
+ <div class="large-space"></div>
30
+ <div class="holder large-padding">
31
+ <h5>More {{ site.title }} features:</h5>
32
+ <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
+ <h5 class="small">
49
+ <strong>{{ feature.title }}</strong>
50
+ </h5>
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>
63
+ </div>
64
+ </div>
65
+ <div class="large-space"></div>
49
66
  </div>
50
67
  </template>
51
68
 
52
69
  <style scoped>
53
- h2 {
54
- margin-top: 8rem;
70
+ .grid > a,
71
+ .grid > a > article {
72
+ height: 100%;
73
+ display: flex;
74
+ flex-direction: column;
75
+ justify-content: space-between;
55
76
  }
56
77
  </style>
@@ -0,0 +1,439 @@
1
+ <template>
2
+ <!-- keep overflow visible so sticky header works -->
3
+ <div class="container">
4
+ <div class="grid">
5
+ <div class="m6 s12 left-align">
6
+ <h6 v-if="!localDetails.length" class="bold">{{ title }}</h6>
7
+ </div>
8
+
9
+ <div :class="`m6 s12 ${isMobile ? 'left-align' : 'right-align'}`">
10
+ <label
11
+ v-for="(option, index) in localOptions"
12
+ :key="option.id"
13
+ class="checkbox"
14
+ >
15
+ <input
16
+ type="checkbox"
17
+ :id="option.id"
18
+ v-model="option.checked"
19
+ @change="updatePrices"
20
+ />
21
+ <span class="margin" :for="option.id"
22
+ ><h6 :class="`bold ${isMobile ? 'medium-text' : ''}`">
23
+ + {{ option.label }}
24
+ </h6></span
25
+ >
26
+ </label>
27
+ </div>
28
+ </div>
29
+ <div
30
+ class="grid m l surface-bright"
31
+ style="position: sticky; top: 72px; z-index: 1000; padding-top: 20px"
32
+ v-if="localDetails.length"
33
+ >
34
+ <div class="s12 m3"></div>
35
+ <div
36
+ v-for="(plan, index) in localPlans"
37
+ :key="`header-${index}`"
38
+ class="s12 m3 center-align"
39
+ >
40
+ <h6 class="primary-text bold">{{ plan.name }}</h6>
41
+ <ClientOnly>
42
+ <a
43
+ :href="addPlanConfig(contactLink, plan)"
44
+ class="button responsive bold margin-top-1 margin-bottom-2"
45
+ >
46
+ Contact us
47
+ </a>
48
+ </ClientOnly>
49
+ </div>
50
+ </div>
51
+
52
+ <h6 v-if="localDetails.length" class="bold padding-4">{{ title }}</h6>
53
+ <!-- Main Plans Table -->
54
+ <div class="wrapper" :style="gridStyle">
55
+ <div class="cell orig-col-1 m l top-margin">
56
+ <h6 v-if="!localDetails.length" class="bold small">Plans:</h6>
57
+ </div>
58
+ <div
59
+ v-for="(plan, index) in localPlans"
60
+ :key="'head-' + index"
61
+ class="cell surface-container-low small-top-round bold top-margin"
62
+ :class="`orig-col-${index + 2}`"
63
+ >
64
+ <h6 class="primary-text bold">{{ plan.name }}</h6>
65
+ </div>
66
+
67
+ <div class="cell orig-col-1 m l">Price (per month):</div>
68
+ <div
69
+ v-for="(plan, index) in localPlans"
70
+ :key="'price-' + index"
71
+ class="surface-container-low primary-text bold"
72
+ :class="`cell orig-col-${index + 2}`"
73
+ >
74
+ <div class="small-round surface-container medium-padding">
75
+ <h4 class="primary-text bold">€ {{ calculatePrice(plan) }},-</h4>
76
+ </div>
77
+ </div>
78
+
79
+ <template
80
+ v-for="(row, rowIndex) in getRowKeys(localPlans)"
81
+ :key="'row-' + rowIndex"
82
+ >
83
+ <div
84
+ v-if="typeof row === 'string'"
85
+ class="cell orig-col-1 m l"
86
+ v-html="row + ':'"
87
+ ></div>
88
+
89
+ <div
90
+ v-for="(plan, planIndex) in localPlans"
91
+ :key="`${planIndex}-${row}`"
92
+ class="bold surface-container-low"
93
+ :class="`cell orig-col-${planIndex + 2} ${rowIndex === getRowKeys(localPlans).length - 1 && !config.showSales ? 'small-bottom-round' : ''}`"
94
+ >
95
+ <div
96
+ v-if="typeof plan.details[row] === 'string'"
97
+ class="s grey-text"
98
+ v-html="row"
99
+ ></div>
100
+ <div
101
+ v-if="typeof plan.details[row] === 'object'"
102
+ :class="`${plan.details[row].alternative ? 'border primary-border small-round small-padding' : ''}`"
103
+ >
104
+ <span v-html="plan.details[row].text"></span>
105
+ <template v-if="plan.details[row].alternative">
106
+ <hr class="medium" />
107
+ <label class="alternative checkbox bold">
108
+ <input
109
+ type="checkbox"
110
+ :id="`${plan.name}-${row}`"
111
+ @input="$forceUpdate()"
112
+ />
113
+ <span :for="`${plan.name}-${row}`"
114
+ ><h6 class="small bold">
115
+ {{ plan.details[row].alternative.name }}
116
+ </h6></span
117
+ >
118
+ </label>
119
+ <span
120
+ :for="`${plan.name}-${row}`"
121
+ v-html="plan.details[row].alternative.text"
122
+ style="white-space: wrap; display: block"
123
+ ></span>
124
+ </template>
125
+ </div>
126
+ <template v-else>
127
+ <span v-html="plan.details[row]"></span>
128
+ </template>
129
+ </div>
130
+ </template>
131
+ <div class="cell orig-col-1 m l"></div>
132
+ <div
133
+ v-for="(plan, index) in localPlans"
134
+ :key="'cta-' + index"
135
+ :class="`surface-container-low small-bottom-round cell orig-col-${index + 2} center-align`"
136
+ >
137
+ <a v-if="plan.link" :href="plan.link"
138
+ >See all features
139
+ <svg
140
+ style="width: 16px; height: 16px"
141
+ xmlns="http://www.w3.org/2000/svg"
142
+ viewBox="0 0 24 24"
143
+ >
144
+ <title>chevron-right</title>
145
+ <path
146
+ d="M8.59,16.58L13.17,12L8.59,7.41L10,6L16,12L10,18L8.59,16.58Z"
147
+ />
148
+ </svg>
149
+ </a>
150
+ </div>
151
+ <!-- Contact us section -->
152
+ <div class="cell orig-col-1 m l"></div>
153
+ <div
154
+ v-if="showSales"
155
+ v-for="(plan, index) in localPlans"
156
+ :key="'cta-' + index"
157
+ :class="`surface-container-low small-bottom-round cell orig-col-${index + 2} center-align`"
158
+ >
159
+ <ClientOnly>
160
+ <a
161
+ v-if="showSales"
162
+ :href="addPlanConfig(contactLink, plan)"
163
+ class="button responsive bold"
164
+ >Contact us</a
165
+ >
166
+ </ClientOnly>
167
+ </div>
168
+ </div>
169
+
170
+ <!-- Add-on Details Tables -->
171
+ <template v-for="(detail, detailIndex) in localDetails" :key="detailIndex">
172
+ <hr class="margin-top-4" />
173
+ <div class="margin-top-2">
174
+ <div class="s12">
175
+ <h5 class="bold">{{ detail.title }}</h5>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="wrapper" :style="secondaryGridStyle">
180
+ <div class="cell cell orig-col-1 m l">
181
+ <div class="">Additional price:</div>
182
+ </div>
183
+ <div
184
+ v-for="(plan, detailPlanIndex) in detail.plans"
185
+ :key="detailPlanIndex"
186
+ class="surface-container-low primary-text bold small-top-round top-margin"
187
+ :class="`cell orig-col-${detailPlanIndex + 2}`"
188
+ >
189
+ <h6 class="primary-text bold s">{{ plan.name }}</h6>
190
+ <h6 v-if="plan.additionalPrice" class="primary-text bold">
191
+ + € {{ plan.additionalPrice }},-
192
+ </h6>
193
+ </div>
194
+ <template
195
+ v-for="(row, rowIndex) in getRowKeys(detail.plans)"
196
+ :key="'row-' + rowIndex"
197
+ >
198
+ <div
199
+ v-if="typeof row === 'string'"
200
+ class="cell orig-col-1 m l"
201
+ v-html="row + ':'"
202
+ ></div>
203
+ <div
204
+ v-for="(plan, planIndex) in detail.plans"
205
+ :key="`${planIndex}-${row}`"
206
+ class="bold surface-container-low"
207
+ :class="`cell orig-col-${planIndex + 2} ${rowIndex === getRowKeys(detail.plans).length - 1 && !config.showSales ? 'small-bottom-round' : ''}`"
208
+ >
209
+ <div class="s grey-text" v-html="row + ':'"></div>
210
+ <template
211
+ v-if="
212
+ typeof plan.details[row] === 'object' &&
213
+ plan.details[row]?.checkmark
214
+ "
215
+ >
216
+ <i class="mdi mdi-check-circle-outline"></i>
217
+ </template>
218
+ <template v-else>
219
+ <span v-html="plan.details[row]"></span>
220
+ </template>
221
+ </div>
222
+ </template>
223
+ </div>
224
+ </template>
225
+
226
+ <p class="small grey-text m-4" v-if="showVAT">
227
+ * All prices are given excluding VAT. Prices are valid until 2025-06-30
228
+ </p>
229
+ </div>
230
+ </template>
231
+
232
+ <script>
233
+ export default {
234
+ name: "PricingTable",
235
+ props: {
236
+ config: {
237
+ type: Object,
238
+ default: () => ({
239
+ title: "Pricing Options",
240
+ showSales: true,
241
+ showVAT: true,
242
+ options: [],
243
+ plans: [],
244
+ details: [],
245
+ }),
246
+ },
247
+ },
248
+ data() {
249
+ return {
250
+ title: "",
251
+ showSales: true,
252
+ contactLink: "",
253
+ showVAT: true,
254
+ localOptions: [],
255
+ localPlans: [],
256
+ localDetails: [],
257
+ isMobile: !import.meta.env.SSR && window.innerWidth <= 768,
258
+ };
259
+ },
260
+ computed: {
261
+ gridStyle() {
262
+ // dynamically asign number of cols in grid when not on mobile
263
+ if (!this.isMobile) {
264
+ return {
265
+ "grid-template-columns": `repeat(${this.localPlans.length + 1}, 1fr)`,
266
+ };
267
+ }
268
+ },
269
+ secondaryGridStyle() {
270
+ if (!this.isMobile && this.localDetails) {
271
+ return {
272
+ "grid-template-columns": `repeat(${this.localDetails[0].plans.length + 1}, 1fr)`,
273
+ };
274
+ }
275
+ },
276
+ },
277
+ methods: {
278
+ calculatePrice(plan) {
279
+ const basePrice = plan.basePrice;
280
+ const additionalPrice = this.localOptions
281
+ .filter((option) => option.checked)
282
+ .reduce((sum, option) => sum + (option.modifier || 0), 0);
283
+
284
+ return basePrice + additionalPrice;
285
+ },
286
+ getRowKeys(plansArray) {
287
+ const keys = new Set();
288
+ plansArray.forEach((plan) => {
289
+ Object.keys(plan.details).forEach((key) => keys.add(key));
290
+ });
291
+ return Array.from(keys);
292
+ },
293
+ updatePrices() {
294
+ this.$forceUpdate();
295
+ },
296
+ initializeData() {
297
+ this.title = this.config.title ?? this.title;
298
+ this.showSales = this.config.showSales ?? this.showSales;
299
+ this.contactLink = this.config.contactLink ?? this.contactLink;
300
+ this.showVAT = this.config.showVAT ?? this.showVAT;
301
+ this.localOptions = this.config.options ?? this.localOptions;
302
+ this.localPlans = this.config.plans ?? this.localPlans;
303
+ this.localDetails = this.config.details ?? this.localDetails;
304
+ },
305
+ handleResize() {
306
+ this.isMobile = window.innerWidth <= 768;
307
+ },
308
+ updateOrderStyles() {
309
+ if (import.meta.env.SSR) {
310
+ return;
311
+ }
312
+ const count = this.localPlans.length + 1;
313
+ for (let i = 1; i <= count; i++) {
314
+ const elements = document.querySelectorAll(`.orig-col-${i}`);
315
+ elements.forEach((el) => {
316
+ el.style.order = this.isMobile ? i : "";
317
+ });
318
+ }
319
+ },
320
+ addPlanConfig(contactLink, plan) {
321
+ if (import.meta.env.SSR) {
322
+ return "";
323
+ }
324
+ const parts = contactLink.split("?");
325
+ let search = parts[1] || "";
326
+ const params = new URLSearchParams(search);
327
+ params.append("plan", plan.name);
328
+ const alternative = Object.entries(plan.details).find(
329
+ ([key, value]) => value.alternative,
330
+ );
331
+ if (alternative) {
332
+ const checked = document.querySelector(
333
+ `[id='${plan.name}-${alternative[0]}']`,
334
+ )?.checked;
335
+ if (checked) {
336
+ params.append("alternative", alternative[1].alternative.name);
337
+ }
338
+ }
339
+ if (this.localOptions.length > 0)
340
+ [
341
+ this.localOptions.forEach((o) => {
342
+ const checked = document.querySelector(`input#${o.id}`)?.checked;
343
+ if (checked) {
344
+ params.append("option", o.label);
345
+ }
346
+ }),
347
+ ];
348
+ search = params.toString();
349
+ return `${parts[0]}${search ? `?${search}` : ""}`;
350
+ },
351
+ },
352
+ created() {
353
+ this.initializeData();
354
+ },
355
+ mounted() {
356
+ window.addEventListener("resize", this.handleResize);
357
+ this.updateOrderStyles();
358
+ },
359
+ beforeDestroy() {
360
+ window.removeEventListener("resize", this.handleResize);
361
+ },
362
+ watch: {
363
+ options: {
364
+ handler() {
365
+ this.initializeData();
366
+ },
367
+ deep: true,
368
+ },
369
+ plans: {
370
+ handler() {
371
+ this.initializeData();
372
+ },
373
+ deep: true,
374
+ },
375
+ details: {
376
+ handler() {
377
+ this.initializeData();
378
+ },
379
+ deep: true,
380
+ },
381
+ localOptions: {
382
+ handler() {
383
+ this.$forceUpdate();
384
+ },
385
+ deep: true,
386
+ },
387
+ isMobile() {
388
+ if (!import.meta.env.SSR) {
389
+ this.updateOrderStyles();
390
+ }
391
+ },
392
+ },
393
+ };
394
+ </script>
395
+
396
+ <style scoped>
397
+ .container {
398
+ padding: 3rem 0;
399
+ z-index: 0;
400
+ }
401
+
402
+ .wrapper {
403
+ display: grid;
404
+ grid-template-columns: 1fr;
405
+ }
406
+
407
+ .cell {
408
+ padding: 8px 16px 24px 16px;
409
+ overflow: hidden;
410
+ }
411
+
412
+ .small-top-round {
413
+ border-top-left-radius: 16px;
414
+ border-top-right-radius: 16px;
415
+ }
416
+
417
+ .small-bottom-round {
418
+ border-bottom-left-radius: 16px;
419
+ border-bottom-right-radius: 16px;
420
+ }
421
+
422
+ @media screen and (min-width: 769px) {
423
+ .wrapper {
424
+ column-gap: 20px;
425
+ row-gap: 0;
426
+ }
427
+
428
+ .cell {
429
+ order: initial;
430
+ margin-top: 0;
431
+ }
432
+ }
433
+
434
+ .alternative:has(input:not(:checked)) + span,
435
+ span:has(+ * + * > input:checked) {
436
+ text-decoration: line-through;
437
+ opacity: 0.5;
438
+ }
439
+ </style>
package/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import DefaultTheme from "vitepress/theme";
2
2
  import FeatureSection from "./components/FeatureSection.vue";
3
3
  import FeaturesGallery from "./components/FeaturesGallery.vue";
4
+ import PricingTable from "./components/PricingTable.vue";
5
+ import CTASection from "./components/CTASection.vue";
4
6
  import Layout from "./Layout.vue";
5
7
  import "./style.css";
6
8
 
@@ -10,6 +12,8 @@ export default {
10
12
  enhanceApp({ app, router, siteData }) {
11
13
  app.component("FeatureSection", FeatureSection);
12
14
  app.component("FeaturesGallery", FeaturesGallery);
15
+ app.component("PricingTable", PricingTable);
16
+ app.component("CTASection", CTASection);
13
17
 
14
18
  router.onAfterRouteChanged = () => {
15
19
  if (!import.meta.env.SSR) {
@@ -42,6 +46,7 @@ export default {
42
46
  :root, body.light {
43
47
  /* EOxUI */
44
48
  --primary: ${siteData.value.themeConfig.theme.primaryColor};
49
+ --secondary: ${siteData.value.themeConfig.theme.secondaryColor};
45
50
  }
46
51
  `),
47
52
  );
package/src/style.css CHANGED
@@ -6,7 +6,6 @@
6
6
 
7
7
  body {
8
8
  margin: 0;
9
- font-size: 1rem;
10
9
  }
11
10
 
12
11
  .VPNav,
@@ -24,11 +23,18 @@ body {
24
23
  .VPContent h2.large,
25
24
  .VPContent h3.large {
26
25
  font-size: normal;
27
- font-weight: normal;
28
26
  line-height: normal;
29
27
  border-top: none;
30
28
  }
31
29
 
30
+ .VPContent h1,
31
+ .VPContent h2,
32
+ .VPContent h3,
33
+ .VPContent h4,
34
+ .VPContent h5 {
35
+ font-weight: bold;
36
+ }
37
+
32
38
  *:has(> nav.top:not(.s, .m, .l)) {
33
39
  padding-block-start: 0;
34
40
  }
@@ -52,8 +58,67 @@ body {
52
58
  .VPPage {
53
59
  margin: auto;
54
60
  width: 100%;
55
- max-width: 1280px;
56
- padding: 48px 24px;
61
+ max-width: 1200px;
62
+ padding: 48px 24px 0;
63
+ }
64
+
65
+ .VPHome {
66
+ margin-bottom: 0 !important;
67
+ padding-left: 0;
68
+ padding-right: 0;
69
+ }
70
+
71
+ .VPHome .vp-doc a {
72
+ font-weight: 400;
73
+ color: inherit;
74
+ }
75
+ .VPHome .vp-doc a.button {
76
+ font-weight: 500;
77
+ color: var(--on-primary);
78
+ }
79
+
80
+ .VPHome .vp-doc h1 {
81
+ font-size: 3.5625rem;
82
+ }
83
+
84
+ .VPHome .vp-doc h2 {
85
+ font-size: 2.8125rem;
86
+ border-top: none;
87
+ margin-block-start: 1rem;
88
+ }
89
+
90
+ .VPHome .vp-doc h3 {
91
+ font-size: 2.25rem;
92
+ }
93
+
94
+ .VPHome .vp-doc h4 {
95
+ font-size: 2rem;
96
+ }
97
+
98
+ .VPHome .vp-doc h5 {
99
+ font-size: 1.75rem;
100
+ }
101
+
102
+ .VPHome .vp-doc h1 a.header-anchor,
103
+ .VPHome .vp-doc h2 a.header-anchor,
104
+ .VPHome .vp-doc h3 a.header-anchor,
105
+ .VPHome .vp-doc h4 a.header-anchor,
106
+ .VPHome .vp-doc h5 a.header-anchor {
107
+ display: none;
108
+ }
109
+
110
+ .VPLocalNav {
111
+ display: none;
112
+ }
113
+
114
+ .vp-doc.container {
115
+ padding-left: 24px;
116
+ padding-right: 24px;
117
+ }
118
+
119
+ /**/
120
+ footer {
121
+ margin-top: 0 !important;
57
122
  }
58
123
 
59
124
  /**/
@@ -66,6 +131,10 @@ button.text:hover,
66
131
  box-shadow: none;
67
132
  }
68
133
 
134
+ :is(button, .button).border {
135
+ border-color: var(--outline-variant);
136
+ }
137
+
69
138
  /* Feature sections */
70
139
  .full-width {
71
140
  width: 100svw;
@@ -86,17 +155,20 @@ button.text:hover,
86
155
  flex-direction: column;
87
156
  align-items: center;
88
157
  justify-content: space-between;
89
- padding: 32px 0;
90
- max-width: 1400px;
158
+ margin-left: auto;
159
+ margin-right: auto;
160
+ max-width: 1200px;
161
+ padding-bottom: 32px;
91
162
  }
92
163
  .feature-section.landing .holder {
164
+ padding: 32px 0;
93
165
  background: var(--surface-container-low);
94
166
  }
95
167
  .feature-section.dark .holder {
96
168
  background: radial-gradient(
97
169
  farthest-corner at 100% 100%,
98
170
  #0f9bff 0%,
99
- #004170 60%,
171
+ var(--primary) 60%,
100
172
  #000c14 100%
101
173
  );
102
174
  color: #fff;
@@ -111,6 +183,11 @@ button.text:hover,
111
183
  .feature-section .text {
112
184
  padding: 48px;
113
185
  }
186
+ .feature-section:not(.landing) .text {
187
+ padding-top: 0;
188
+ padding-left: 24px;
189
+ padding-right: 24px;
190
+ }
114
191
  .feature-section .icon {
115
192
  background: #61616133;
116
193
  padding: 16px;
@@ -125,35 +202,27 @@ button.text:hover,
125
202
  }
126
203
  .feature-section img {
127
204
  max-width: 80%;
128
- border-radius: 8px;
129
205
  background: var(--surface-bright);
130
- box-shadow: 0px 11px 15px 0px #00000033;
131
206
  }
132
207
  @media (min-width: 1024px) {
133
- .feature-section {
208
+ .feature-section.landing,
209
+ .cta-section {
134
210
  padding: 64px;
135
211
  padding: clamp(64px, 10%, 128px);
136
212
  }
137
- .feature-section:not(.feature-section.landing) {
138
- padding-top: 0;
139
- }
140
213
  .feature-section .holder {
141
214
  flex-direction: row;
142
215
  padding: 68px 0;
143
216
  border-radius: 12px;
144
217
  }
218
+ .feature-section:not(.feature-section.landing) .holder {
219
+ padding-top: 0;
220
+ padding-bottom: 120px;
221
+ }
145
222
  .feature-section img {
146
223
  max-width: 60%;
147
224
  transform: translateX(10%);
148
225
  }
149
- .feature-section.reverse.dark .holder {
150
- background: radial-gradient(
151
- farthest-corner at 0% 100%,
152
- #0f9bff 0%,
153
- #004170 60%,
154
- #000c14 100%
155
- );
156
- }
157
226
  .feature-section.reverse .text {
158
227
  order: 1;
159
228
  }
@@ -166,10 +235,10 @@ button.text:hover,
166
235
  }
167
236
  }
168
237
  @media (min-width: 1280px) {
169
- .feature-section {
170
- padding: 128px;
238
+ .feature-section.landing {
239
+ padding: 128px 24px;
171
240
  }
172
- .feature-section .holder {
241
+ .feature-section.landing .holder {
173
242
  flex-direction: row;
174
243
  padding: 68px 0;
175
244
  }
@@ -223,3 +292,12 @@ button.text:hover,
223
292
  }
224
293
  }
225
294
  }
295
+
296
+ .primary-gradient-background {
297
+ background: radial-gradient(
298
+ 74.48% 130.95% at 50% -12.27%,
299
+ hsl(from var(--primary) h s calc(l + 7)) 0%,
300
+ var(--primary) 45%,
301
+ hsl(from var(--primary) h s calc(l - 20)) 100%
302
+ ) !important;
303
+ }
@@ -14,6 +14,8 @@ export const generate = (brandConfig) => ({
14
14
  siteTitle: false,
15
15
  theme: {
16
16
  primaryColor: brandConfig.theme?.primary_color,
17
+ secondaryColor:
18
+ brandConfig.theme?.secondary_color || brandConfig.theme?.primary_color,
17
19
  },
18
20
  logo: brandConfig.logo,
19
21
  footer: {