@eox/pages-theme-eox 2.0.0-alpha.8 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eox/pages-theme-eox",
3
- "version": "2.0.0-alpha.8",
3
+ "version": "2.0.0-alpha.9",
4
4
  "type": "module",
5
5
  "description": "Vitepress Theme with EOX branding",
6
6
  "main": "src/index.js",
@@ -383,6 +383,14 @@ if (!import.meta.env.SSR) {
383
383
  }
384
384
  .Layout.layout-home .top-nav.at-top .nav-desktop .button.text.active,
385
385
  .top-nav.has-hero.at-top .nav-desktop .button.text.active {
386
+ background-color: rgba(255, 255, 255, 0.15) !important;
387
+ color: #ffffff !important;
388
+ }
389
+ .Layout.layout-home .top-nav.at-top .nav-desktop .button.text.active:hover,
390
+ .top-nav.has-hero.at-top .nav-desktop .button.text.active:hover {
391
+ background-color: rgba(255, 255, 255, 0.25) !important;
392
+ }
393
+ .top-nav:not(.at-top) .nav-desktop .button.text.active {
386
394
  background-color: var(--primary-container) !important;
387
395
  color: var(--on-primary-container) !important;
388
396
  }
@@ -394,6 +402,11 @@ if (!import.meta.env.SSR) {
394
402
  }
395
403
  .Layout.layout-home .top-nav.at-top .nav-desktop .button.border.active,
396
404
  .top-nav.has-hero.at-top .nav-desktop .button.border.active {
405
+ background-color: rgba(255, 255, 255, 0.15) !important;
406
+ color: #ffffff !important;
407
+ border-color: transparent !important;
408
+ }
409
+ .top-nav:not(.at-top) .nav-desktop .button.border.active {
397
410
  background-color: var(--primary-container) !important;
398
411
  color: var(--on-primary-container) !important;
399
412
  border-color: transparent !important;
@@ -20,7 +20,7 @@
20
20
  />
21
21
  </section>
22
22
 
23
- <!-- Team Grid - max 3 columns on large screens -->
23
+ <!-- Team Grid - up to 4 columns on large screens -->
24
24
  <div class="team-grid">
25
25
  <FeatureCard
26
26
  v-for="member in sortedTeamMembers"
@@ -126,7 +126,7 @@ const memberHasPosts = (memberId) => {
126
126
  <style scoped>
127
127
  .team-page {
128
128
  width: 100%;
129
- max-width: 1200px !important;
129
+ max-width: 1400px !important;
130
130
  margin-left: auto !important;
131
131
  margin-right: auto !important;
132
132
  padding-left: 1rem !important;
@@ -245,6 +245,12 @@ const memberHasPosts = (memberId) => {
245
245
  }
246
246
  }
247
247
 
248
+ @media (min-width: 1280px) {
249
+ .team-grid {
250
+ grid-template-columns: repeat(4, minmax(0, 1fr));
251
+ }
252
+ }
253
+
248
254
  /* Deep overrides to integrate with theme-provided FeatureCard */
249
255
  .team-member-card {
250
256
  height: 100% !important;
@@ -0,0 +1,61 @@
1
+ import TextSection from "./TextSection.vue";
2
+
3
+ describe("<TextSection />", () => {
4
+ it("renders title, tagline and button", () => {
5
+ cy.mount(TextSection, {
6
+ props: {
7
+ title: "Test Section",
8
+ tagline: "Test Tagline",
9
+ buttonText: "Click Me",
10
+ buttonLink: "https://example.com",
11
+ },
12
+ });
13
+
14
+ cy.contains("Test Section").should("exist");
15
+ cy.contains("Test Tagline").should("exist");
16
+ cy.contains("a.button", "Click Me")
17
+ .should("have.attr", "href", "https://example.com")
18
+ .and("have.attr", "target", "_blank");
19
+ });
20
+
21
+ it("renders slot content and image", () => {
22
+ cy.mount(TextSection, {
23
+ props: {
24
+ image: "/test.jpg",
25
+ imageAlt: "Test Image",
26
+ },
27
+ slots: {
28
+ default: "<p>This is paragraph text.</p>",
29
+ },
30
+ });
31
+
32
+ cy.contains("This is paragraph text.").should("exist");
33
+ cy.get("img.section-image")
34
+ .should("have.attr", "src", "/test.jpg")
35
+ .and("have.attr", "alt", "Test Image");
36
+ });
37
+
38
+ it("applies reverse class when reverse prop is true", () => {
39
+ cy.mount(TextSection, {
40
+ props: {
41
+ image: "/test.jpg",
42
+ reverse: true,
43
+ },
44
+ });
45
+
46
+ cy.get(".text-section").should("have.class", "is-reverse");
47
+ cy.get(".text-section-grid").should("have.class", "grid-reverse");
48
+ });
49
+
50
+ it("applies single column styling when no image is provided", () => {
51
+ cy.mount(TextSection, {
52
+ slots: {
53
+ default: "<p>Single column text</p>",
54
+ },
55
+ });
56
+
57
+ cy.get(".text-section").should("have.class", "no-image");
58
+ cy.get(".text-section-grid").should("have.class", "grid-single");
59
+ cy.get(".media-column").should("not.exist");
60
+ });
61
+ });
@@ -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>
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) {