@eox/pages-theme-eox 2.0.0-alpha.7 → 2.0.0-alpha.9

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.
@@ -0,0 +1,384 @@
1
+ <template>
2
+ <section
3
+ class="text-section full-width"
4
+ :class="{
5
+ 'is-reverse': reverse,
6
+ 'no-image': !image && !$slots.media,
7
+ 'is-compact': compact,
8
+ }"
9
+ >
10
+ <div class="holder">
11
+ <div
12
+ class="text-section-grid"
13
+ :class="{
14
+ 'grid-reverse': reverse,
15
+ 'grid-single': !image && !$slots.media,
16
+ }"
17
+ >
18
+ <!-- Text Column -->
19
+ <div class="text-column">
20
+ <span v-if="tagline" class="tagline">{{ tagline }}</span>
21
+ <h2 v-if="title" class="section-title">{{ title }}</h2>
22
+ <div class="section-content">
23
+ <slot></slot>
24
+ </div>
25
+ <!-- Prop-based button -->
26
+ <div v-if="buttonText && buttonLink" class="section-actions">
27
+ <a
28
+ :href="resolvedButtonLink"
29
+ :target="resolvedButtonTarget"
30
+ class="button"
31
+ @click="
32
+ trackEvent([
33
+ 'TextSection',
34
+ 'Click',
35
+ title || buttonText,
36
+ buttonText,
37
+ ])
38
+ "
39
+ >
40
+ <span>{{ buttonText }}</span>
41
+ <i v-if="buttonIcon" :class="buttonIcon" class="btn-icon"></i>
42
+ </a>
43
+ </div>
44
+ <slot name="actions"></slot>
45
+ </div>
46
+
47
+ <!-- Media Column -->
48
+ <div v-if="image || $slots.media" class="media-column">
49
+ <slot name="media">
50
+ <img
51
+ v-if="image"
52
+ :src="resolvedImage"
53
+ :alt="imageAlt || title || ''"
54
+ class="section-image"
55
+ loading="lazy"
56
+ />
57
+ </slot>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </section>
62
+ </template>
63
+
64
+ <script setup>
65
+ import { withBase } from "vitepress";
66
+ import { computed } from "vue";
67
+ import { trackEvent } from "../helpers.js";
68
+
69
+ const props = defineProps({
70
+ title: {
71
+ type: String,
72
+ default: "",
73
+ },
74
+ tagline: {
75
+ type: String,
76
+ default: "",
77
+ },
78
+ image: {
79
+ type: String,
80
+ default: "",
81
+ },
82
+ imageAlt: {
83
+ type: String,
84
+ default: "",
85
+ },
86
+ reverse: {
87
+ type: Boolean,
88
+ default: false,
89
+ },
90
+ buttonText: {
91
+ type: String,
92
+ default: "",
93
+ },
94
+ buttonLink: {
95
+ type: String,
96
+ default: "",
97
+ },
98
+ buttonTarget: {
99
+ type: String,
100
+ default: "",
101
+ },
102
+ buttonIcon: {
103
+ type: String,
104
+ default: "",
105
+ },
106
+ compact: {
107
+ type: Boolean,
108
+ default: false,
109
+ },
110
+ });
111
+
112
+ function resolveUrl(url) {
113
+ if (!url) return "";
114
+ if (
115
+ url.startsWith("http://") ||
116
+ url.startsWith("https://") ||
117
+ url.startsWith("//") ||
118
+ url.startsWith("#")
119
+ ) {
120
+ return url;
121
+ }
122
+ return withBase(url);
123
+ }
124
+
125
+ const resolvedImage = computed(() => resolveUrl(props.image));
126
+ const resolvedButtonLink = computed(() => resolveUrl(props.buttonLink));
127
+ const resolvedButtonTarget = computed(() => {
128
+ if (props.buttonTarget) return props.buttonTarget;
129
+ if (
130
+ props.buttonLink &&
131
+ (props.buttonLink.startsWith("http://") ||
132
+ props.buttonLink.startsWith("https://") ||
133
+ props.buttonLink.startsWith("//"))
134
+ ) {
135
+ return "_blank";
136
+ }
137
+ return "_self";
138
+ });
139
+ </script>
140
+
141
+ <style scoped>
142
+ .full-width {
143
+ width: 100svw;
144
+ position: relative;
145
+ left: 50%;
146
+ right: 50%;
147
+ margin-left: -50svw;
148
+ margin-right: -50svw;
149
+ max-inline-size: unset !important;
150
+ }
151
+
152
+ .text-section {
153
+ padding-block: 56px;
154
+ display: flex;
155
+ justify-content: center;
156
+ background: transparent;
157
+ color: var(--on-surface, #020617);
158
+ }
159
+
160
+ .text-section.is-compact {
161
+ padding-block: 28px;
162
+ }
163
+
164
+ /* When two text-sections are placed consecutively, tighten the spacing */
165
+ :deep(.text-section + .text-section),
166
+ .text-section + .text-section {
167
+ padding-block-start: 16px;
168
+ }
169
+
170
+ .text-section .holder {
171
+ width: 100%;
172
+ max-width: 1200px;
173
+ margin-left: auto;
174
+ margin-right: auto;
175
+ padding-inline: 24px;
176
+ }
177
+
178
+ .text-section-grid {
179
+ display: grid;
180
+ grid-template-columns: 1fr;
181
+ gap: 2rem;
182
+ align-items: center;
183
+ }
184
+
185
+ /* On mobile (< 1024px), the media/image column is always at the start before the text */
186
+ .text-section-grid .media-column {
187
+ order: 1;
188
+ }
189
+
190
+ .text-section-grid .text-column {
191
+ order: 2;
192
+ }
193
+
194
+ @media (min-width: 1024px) {
195
+ .text-section-grid {
196
+ grid-template-columns: 1fr 1fr;
197
+ gap: 4rem;
198
+ }
199
+
200
+ /* Default on desktop: Text on left (1), Media on right (2) */
201
+ .text-section-grid .text-column {
202
+ order: 1;
203
+ }
204
+
205
+ .text-section-grid .media-column {
206
+ order: 2;
207
+ }
208
+
209
+ /* Reverse on desktop: Media on left (1), Text on right (2) */
210
+ .text-section-grid.grid-reverse .text-column {
211
+ order: 2;
212
+ }
213
+
214
+ .text-section-grid.grid-reverse .media-column {
215
+ order: 1;
216
+ }
217
+
218
+ .text-section-grid.grid-single {
219
+ grid-template-columns: 1fr;
220
+ max-width: 680px;
221
+ }
222
+ }
223
+
224
+ .text-column {
225
+ display: flex;
226
+ flex-direction: column;
227
+ width: 100%;
228
+ }
229
+
230
+ .tagline {
231
+ display: inline-block;
232
+ font-size: 0.875rem;
233
+ font-weight: 600;
234
+ text-transform: uppercase;
235
+ letter-spacing: 0.05em;
236
+ color: var(--primary, #004170);
237
+ margin-bottom: 0.5rem;
238
+ }
239
+
240
+ .section-title {
241
+ font-family: inherit;
242
+ font-weight: 700;
243
+ font-size: 2rem;
244
+ line-height: 1.25;
245
+ color: #020617;
246
+ margin-top: 0;
247
+ margin-bottom: 1.25rem;
248
+ border-top: none;
249
+ }
250
+
251
+ /* Markdown Content Styling */
252
+ .section-content :deep(h2) {
253
+ font-family: inherit;
254
+ font-weight: 700;
255
+ font-size: 2rem;
256
+ line-height: 1.25;
257
+ color: #020617;
258
+ margin-top: 0;
259
+ margin-bottom: 1.25rem;
260
+ border-top: none;
261
+ }
262
+
263
+ .section-content :deep(h3) {
264
+ font-family: inherit;
265
+ font-weight: 700;
266
+ font-size: 1.5rem;
267
+ line-height: 1.35;
268
+ color: #020617;
269
+ margin-top: 1.5rem;
270
+ margin-bottom: 0.75rem;
271
+ border-top: none;
272
+ }
273
+
274
+ .section-content :deep(h4),
275
+ .section-content :deep(h5) {
276
+ font-family: inherit;
277
+ font-weight: 600;
278
+ font-size: 1.125rem;
279
+ line-height: 1.4;
280
+ color: #020617;
281
+ margin-top: 1.25rem;
282
+ margin-bottom: 0.5rem;
283
+ }
284
+
285
+ .section-content :deep(p) {
286
+ font-size: 1rem;
287
+ line-height: 1.65;
288
+ color: #334155;
289
+ margin-top: 0;
290
+ margin-bottom: 1rem;
291
+ }
292
+
293
+ .section-content :deep(ul) {
294
+ margin-top: 0.75rem;
295
+ margin-bottom: 1.25rem;
296
+ padding-left: 1.25rem;
297
+ list-style-type: disc;
298
+ }
299
+
300
+ .section-content :deep(li) {
301
+ font-size: 0.95rem;
302
+ line-height: 1.6;
303
+ color: #334155;
304
+ margin-bottom: 0.5rem;
305
+ }
306
+
307
+ .section-content :deep(li strong) {
308
+ color: #020617;
309
+ font-weight: 700;
310
+ }
311
+
312
+ .section-content :deep(a:not(.button)) {
313
+ color: #0284c7;
314
+ text-decoration: none;
315
+ font-weight: 500;
316
+ transition: color 0.15s ease-in-out;
317
+ }
318
+
319
+ .section-content :deep(a:not(.button):hover) {
320
+ text-decoration: underline;
321
+ color: #0369a1;
322
+ }
323
+
324
+ /* Pill Button Styling - for both prop and markdown links with .button */
325
+ .section-actions {
326
+ margin-top: 0.75rem;
327
+ margin-bottom: 0.5rem;
328
+ }
329
+
330
+ .button,
331
+ .section-content :deep(a.button),
332
+ .section-content :deep(.button) {
333
+ display: inline-flex;
334
+ align-items: center;
335
+ justify-content: center;
336
+ gap: 0.5rem;
337
+ padding: 0.5rem 1.5rem;
338
+ border-radius: 9999px;
339
+ border: 1px solid #020617;
340
+ background-color: #ffffff;
341
+ color: #020617 !important;
342
+ font-weight: 600;
343
+ font-size: 0.9375rem;
344
+ line-height: 1.5;
345
+ text-decoration: none !important;
346
+ transition: all 0.2s ease-in-out;
347
+ cursor: pointer;
348
+ width: fit-content;
349
+ }
350
+
351
+ .button:hover,
352
+ .section-content :deep(a.button:hover),
353
+ .section-content :deep(.button:hover) {
354
+ background-color: #f1f5f9;
355
+ border-color: #020617;
356
+ color: #020617 !important;
357
+ }
358
+
359
+ .btn-icon {
360
+ font-size: 1.1em;
361
+ display: inline-flex;
362
+ }
363
+
364
+ /* Media Column & Image Styling */
365
+ .media-column {
366
+ display: flex;
367
+ justify-content: center;
368
+ align-items: center;
369
+ width: 100%;
370
+ }
371
+
372
+ .section-image {
373
+ width: 100%;
374
+ max-width: 100%;
375
+ height: auto;
376
+ border-radius: 16px;
377
+ border: 1px solid rgba(15, 23, 42, 0.08);
378
+ box-shadow:
379
+ 0 4px 16px -2px rgba(15, 23, 42, 0.06),
380
+ 0 2px 6px -2px rgba(15, 23, 42, 0.04);
381
+ background-color: #ffffff;
382
+ display: block;
383
+ }
384
+ </style>
@@ -98,8 +98,9 @@ defineProps({
98
98
  :deep(samp),
99
99
  :deep(var),
100
100
  :deep(.monospace) {
101
- font-family: "SFMono-Regular", "Menlo", "Monaco", "Consolas",
102
- "Liberation Mono", "Ubuntu Mono", "Courier New", monospace !important;
101
+ font-family:
102
+ "SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono",
103
+ "Ubuntu Mono", "Courier New", monospace !important;
103
104
  }
104
105
 
105
106
  /* Inline code */
package/src/helpers.js CHANGED
@@ -283,6 +283,8 @@ export const getFlatList = (items) => {
283
283
  * @returns {boolean}
284
284
  */
285
285
  export const isActive = (item, path) => {
286
+ if (!item || !path) return false;
287
+
286
288
  if (item.activeMatch) {
287
289
  try {
288
290
  return new RegExp(item.activeMatch).test(path);
@@ -292,20 +294,26 @@ export const isActive = (item, path) => {
292
294
  }
293
295
  }
294
296
 
295
- if (item.items) {
296
- return item.items.some((sub) => isActive(sub, path));
297
+ if (item.items && item.items.some((sub) => isActive(sub, path))) {
298
+ return true;
297
299
  }
298
300
 
299
301
  if (item.link) {
300
- const linkPath = item.link.replace(/\.html$/, "").replace(/\/$/, "");
301
- const currentPath = path.replace(/\.html$/, "").replace(/\/$/, "");
302
+ const normalize = (p) =>
303
+ decodeURI(p)
304
+ .replace(/[?#].*$/, "")
305
+ .replace(/\.html$/, "")
306
+ .replace(/\/$/, "");
307
+
308
+ const linkPath = normalize(item.link);
309
+ const currentPath = normalize(path);
302
310
 
303
311
  if (linkPath === currentPath) return true;
304
312
 
305
313
  // Special case for home page to avoid matching everything
306
314
  if (linkPath === "") return currentPath === "";
307
315
 
308
- return currentPath.startsWith(linkPath);
316
+ return currentPath === linkPath || currentPath.startsWith(linkPath + "/");
309
317
  }
310
318
 
311
319
  return false;
@@ -144,10 +144,38 @@ describe("existing helpers", () => {
144
144
  t("nav.unknown", { nav: { home: "Home Page" } }),
145
145
  "nav.unknown",
146
146
  );
147
+ assert.equal(t("Imprint", { Imprint: "Impressum" }), "Impressum");
148
+ assert.equal(t("Imprint", {}), "Imprint");
147
149
  });
148
150
 
149
151
  it("checks isActive links correctly", () => {
150
152
  assert.equal(isActive({ link: "/explore/" }, "/explore"), true);
151
153
  assert.equal(isActive({ link: "/explore" }, "/explore/"), true);
154
+ assert.equal(isActive({ link: "/explore" }, "/explore/subpage"), true);
155
+ assert.equal(isActive({ link: "/explore" }, "/explore-other"), false);
156
+ assert.equal(isActive({ link: "/explore" }, "/explore?tab=1#sec"), true);
157
+ assert.equal(isActive({ link: "/" }, "/"), true);
158
+ assert.equal(isActive({ link: "/" }, "/explore"), false);
159
+
160
+ // Parent dropdown with items
161
+ const parentDropdown = {
162
+ text: "Expertise",
163
+ items: [
164
+ { text: "Data Management", link: "/expertise/data-management" },
165
+ { text: "Data Visualization", link: "/expertise/data-visualization" },
166
+ ],
167
+ };
168
+ assert.equal(isActive(parentDropdown, "/expertise/data-management"), true);
169
+ assert.equal(isActive(parentDropdown, "/expertise/data-management/"), true);
170
+ assert.equal(isActive(parentDropdown, "/solutions"), false);
171
+
172
+ // Parent with both link and items
173
+ const parentWithLinkAndItems = {
174
+ text: "Products",
175
+ link: "/products",
176
+ items: [{ text: "Product A", link: "/products/a" }],
177
+ };
178
+ assert.equal(isActive(parentWithLinkAndItems, "/products/a"), true);
179
+ assert.equal(isActive(parentWithLinkAndItems, "/products"), true);
152
180
  });
153
181
  });
package/src/index.js CHANGED
@@ -23,6 +23,7 @@ import BlogList from "./components/BlogList.vue";
23
23
  import TeamPage from "./components/TeamPage.vue";
24
24
  import SectionHeader from "./components/SectionHeader.vue";
25
25
  import TestimonialsSection from "./components/TestimonialsSection.vue";
26
+ import TextSection from "./components/TextSection.vue";
26
27
  import Layout from "./Layout.vue";
27
28
  import "./style.css";
28
29
 
@@ -55,6 +56,7 @@ export default {
55
56
  app.component("TeamPage", TeamPage);
56
57
  app.component("SectionHeader", SectionHeader);
57
58
  app.component("TestimonialsSection", TestimonialsSection);
59
+ app.component("TextSection", TextSection);
58
60
 
59
61
  router.onAfterRouteChanged = () => {
60
62
  if (!import.meta.env.SSR) {
@@ -1,4 +1,5 @@
1
1
  ---
2
2
  layout: blog
3
3
  title: Author posts
4
+ hero: true
4
5
  ---
package/src/pages/blog.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ---
2
2
  layout: blog
3
3
  title: Blog
4
+ hero: true
4
5
  ---
@@ -1,4 +1,5 @@
1
1
  ---
2
2
  layout: blog
3
3
  title: Tag list
4
+ hero: true
4
5
  ---
package/src/pages/tag.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ---
2
2
  layout: tags
3
3
  title: Tags
4
+ hero: true
4
5
  ---
package/src/pages/team.md CHANGED
@@ -1,4 +1,5 @@
1
1
  ---
2
2
  layout: team
3
3
  title: Team
4
+ hero: true
4
5
  ---
package/src/style.css CHANGED
@@ -224,7 +224,8 @@ footer {
224
224
  margin-top: 0 !important;
225
225
  position: relative;
226
226
  z-index: var(--vp-z-index-footer);
227
- background: var(--surface-container-low);
227
+ background: var(--surface-container-lowest);
228
+ border-top: 1px solid var(--vp-c-divider, rgba(0, 0, 0, 0.08));
228
229
  }
229
230
 
230
231
  /**/
@@ -249,14 +250,24 @@ button.text:hover,
249
250
  }
250
251
  }
251
252
 
252
- .primary-gradient-bg {
253
+ .hero-gradient-bg {
253
254
  /*Fallback*/
254
- background: var(--primary);
255
- background: radial-gradient(
256
- 74.48% 130.95% at 50% -12.27%,
257
- hsl(from var(--primary) h s calc(l + 7)) 0%,
258
- var(--primary) 45%,
259
- hsl(from var(--primary) h s calc(l - 20)) 100%
255
+ background: #000c14;
256
+ background: linear-gradient(
257
+ 180deg,
258
+ #000c14 0%,
259
+ var(--primary, #004170) 100%
260
+ ) !important;
261
+ }
262
+
263
+ .primary-gradient-bg,
264
+ .dark-section-gradient-bg {
265
+ /*Fallback*/
266
+ background: #000c14;
267
+ background: linear-gradient(
268
+ 180deg,
269
+ var(--primary, #004170) 0%,
270
+ #000c14 100%
260
271
  ) !important;
261
272
  }
262
273
 
@@ -1,6 +1,5 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { resolveLlms } from "./llms/index.js";
4
3
 
5
4
  function copyThemePages() {
6
5
  try {
@@ -94,7 +93,6 @@ function getBlogRewrites() {
94
93
  */
95
94
  export const generate = (brandConfig) => {
96
95
  copyThemePages();
97
- const llmsResult = resolveLlms(brandConfig.llms, brandConfig);
98
96
  const config = {
99
97
  appearance: false,
100
98
  cleanUrls: true,
@@ -193,9 +191,6 @@ export const generate = (brandConfig) => {
193
191
  themeConfig: {
194
192
  outline: "deep",
195
193
  siteTitle: false,
196
- llms: {
197
- copyButtons: llmsResult.copyButtons,
198
- },
199
194
  theme: {
200
195
  primaryColor: brandConfig.theme?.primary_color,
201
196
  secondaryColor:
@@ -228,7 +223,6 @@ export const generate = (brandConfig) => {
228
223
  optimizeDeps: {
229
224
  exclude: ["@eox/pages-theme-eox"],
230
225
  },
231
- plugins: [...llmsResult.plugins],
232
226
  },
233
227
  vue: {
234
228
  template: {
package/src/llms/index.js DELETED
@@ -1,64 +0,0 @@
1
- import llmstxt from "vitepress-plugin-llms";
2
- import { createStoriesPlugin } from "./storiesPlugin.js";
3
-
4
- const LLMS_DEFAULTS = {};
5
-
6
- /**
7
- * Normalizes a URL by stripping trailing slashes.
8
- * @param {string | undefined | null} url
9
- * @returns {string | null}
10
- */
11
- export const normalizeDomain = (url) => {
12
- if (!url || typeof url !== "string") return null;
13
- return url.replace(/\/+$/, "");
14
- };
15
-
16
- /**
17
- * Resolve the llms option into Vite plugins and package-owned flags.
18
- * @param {boolean | Record<string, any>} [option] The `llms` option
19
- * @param {Record<string, any>} [brandConfig] Brand configuration object
20
- * @returns {{ plugins: any[], copyButtons: boolean, stories: any, domain: string | null }}
21
- */
22
- export const resolveLlms = (option, brandConfig = {}) => {
23
- if (option === false) {
24
- return {
25
- plugins: [],
26
- copyButtons: false,
27
- stories: null,
28
- domain: null,
29
- };
30
- }
31
-
32
- const {
33
- copyButtons = false,
34
- stories = null,
35
- ...settings
36
- } = option === true || option === undefined ? {} : option;
37
-
38
- const rawDomain = settings.domain;
39
- const domain = normalizeDomain(rawDomain);
40
-
41
- const upstreamPlugins = llmstxt({
42
- ...LLMS_DEFAULTS,
43
- ...settings,
44
- ...(domain ? { domain } : {}),
45
- });
46
-
47
- const plugins = [...upstreamPlugins];
48
- if (stories !== false) {
49
- plugins.push(
50
- createStoriesPlugin({
51
- storiesOption: stories,
52
- brandConfig,
53
- domain,
54
- }),
55
- );
56
- }
57
-
58
- return {
59
- plugins,
60
- copyButtons: Boolean(copyButtons),
61
- stories,
62
- domain,
63
- };
64
- };