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