@eox/pages-theme-eox 1.1.1 → 1.2.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/CHANGELOG.md +16 -0
- package/README.md +79 -4
- package/cypress/support/mocks/features.data.js +1 -0
- package/package.json +2 -2
- package/src/components/FeatureCard/Index.cy.js +156 -0
- package/src/components/FeatureCard/Index.vue +163 -0
- package/src/components/FeatureCard/Poster.cy.js +137 -0
- package/src/components/FeatureCard/Poster.vue +172 -0
- package/src/components/FeaturesGallery.cy.js +114 -4
- package/src/components/FeaturesGallery.vue +78 -25
- package/src/components/NavBar.vue +15 -12
- package/src/components/PricingTable.vue +4 -4
- package/src/vitepressConfig.mjs +1 -1
- package/src/components/FeatureCard.cy.js +0 -83
- package/src/components/FeatureCard.vue +0 -82
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.1.2...v1.2.0) (2026-03-19)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- introduce FeatureCards variants ([#10](https://gitlab.eox.at/eox/hub/eoxhub-portal/pages-theme-eox/-/merge_requests/10))
|
|
8
|
+
|
|
9
|
+
### Miscellaneous chores
|
|
10
|
+
|
|
11
|
+
- update @eox/ui and add required adjustments ([3dae8d2d](https://gitlab.eox.at/eox/hub/eoxhub-portal/pages-theme-eox/-/commit/3dae8d2d2d2619667e8334c91d492def4981421b))
|
|
12
|
+
|
|
13
|
+
## [1.1.2](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.1.1...v1.1.2) (2026-03-16)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
- update eoxhub logo url ([bc39f8ea](https://gitlab.eox.at/eox/hub/eoxhub-portal/commit/bc39f8eae6bdf0c3eb655629d3da53ca88f40d2a))
|
|
18
|
+
|
|
3
19
|
## [1.1.1](https://gitlab.eox.at/eox/hub/eoxhub-portal/compare/v1.1.0...v1.1.1) (2026-03-02)
|
|
4
20
|
|
|
5
21
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -270,22 +270,97 @@ An interactive table with expandable rows. The `summary` object keys must match
|
|
|
270
270
|
|
|
271
271
|
A grid layout for feature cards. Automatically used on pages under `features/`, but can be used manually with custom cards.
|
|
272
272
|
|
|
273
|
+
##### Gallery Props
|
|
274
|
+
|
|
275
|
+
| Prop | Type | Default | Description |
|
|
276
|
+
| :--------------- | :---------------- | :------------------------------ | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
277
|
+
| `sectionTitle` | `String\|Boolean` | `"More {site.title} features:"` | Title displayed above the grid. Set to `false` to hide it. |
|
|
278
|
+
| `variant` | `String` | `"default"` | Card variant to use. Supported values: `"default"`, `"poster"`. |
|
|
279
|
+
| `columns` | `String\|Number` | `"4/2/1"` | Number of columns at large/medium/small breakpoints in `"L/M/S"` format, e.g. `"3/2/1"`. A single number applies to large only (medium defaults to 2, small to 1). |
|
|
280
|
+
| `cards` | `Array` | — | Array of card objects (see **Card Properties** below). Falls back to auto-collected `<FeatureSection>` data if omitted. |
|
|
281
|
+
| `background` | `String` | `"primary primary-gradient-bg"` | CSS class(es) applied to the section background. |
|
|
282
|
+
| `cardBackground` | `String` | `"surface-container-low"` | CSS class(es) applied to each card. |
|
|
283
|
+
|
|
284
|
+
##### Card Properties
|
|
285
|
+
|
|
286
|
+
Each object in the `cards` array supports the following properties:
|
|
287
|
+
|
|
288
|
+
| Property | Type | Required | Description |
|
|
289
|
+
| :-------------- | :---------------------------------- | :------- | :-------------------------------------------------------------------------------------------------------------------------- |
|
|
290
|
+
| `id` | `String\|Number` | ✅ | Unique identifier for the card. |
|
|
291
|
+
| `title` | `String` | ✅ | Card heading text. |
|
|
292
|
+
| `content` | `String` | ✅ | Card body text (supports HTML). |
|
|
293
|
+
| `icon` | `String\|{ html, width?, height? }` | — | Icon to display. Use an `mdi-*` class string for Material Design icons, or an object with `html` for custom SVG/HTML icons. |
|
|
294
|
+
| `image` | `String` | — | URL of an image. Used as a fallback icon in the `default` variant, and as a background in the `poster` variant. |
|
|
295
|
+
| `chips` | `{ text, class? }[]` | — | Array of chip/badge objects rendered above the title. |
|
|
296
|
+
| `metadata` | `{ text, icon? }[]` | — | Array of metadata items (e.g. date, author) rendered with an optional `mdi-*` icon. |
|
|
297
|
+
| `link` | `{ text, href, target? }` | — | Primary call-to-action link. `target` defaults to `_blank`. |
|
|
298
|
+
| `secondaryLink` | `{ text, href, target? }` | — | Secondary link rendered alongside the primary link. `target` defaults to `_blank`. |
|
|
299
|
+
|
|
300
|
+
##### Card Variants
|
|
301
|
+
|
|
302
|
+
###### `default` (default)
|
|
303
|
+
|
|
304
|
+
A standard content card with an icon or image, title, optional chips, metadata, body text and links.
|
|
305
|
+
|
|
273
306
|
```html
|
|
274
307
|
<FeaturesGallery
|
|
275
308
|
section-title="All Features"
|
|
309
|
+
variant="default"
|
|
310
|
+
columns="3/2/1"
|
|
276
311
|
background="surface-container-low"
|
|
277
312
|
:cards="[
|
|
278
313
|
{
|
|
279
314
|
id: 1,
|
|
280
315
|
title: 'Global Coverage',
|
|
281
|
-
content: 'Access data from anywhere.',
|
|
282
|
-
icon: 'mdi-earth'
|
|
316
|
+
content: 'Access satellite data from anywhere on the globe.',
|
|
317
|
+
icon: 'mdi-earth',
|
|
318
|
+
chips: [{ text: 'New', class: 'primary' }],
|
|
319
|
+
metadata: [{ text: '2024-01-15', icon: 'mdi-calendar' }],
|
|
320
|
+
link: { text: 'Learn more', href: '/coverage' },
|
|
321
|
+
secondaryLink: { text: 'API docs', href: '/api', target: '_self' }
|
|
283
322
|
},
|
|
284
323
|
{
|
|
285
324
|
id: 2,
|
|
286
|
-
title: 'Real-time',
|
|
287
|
-
content: '
|
|
325
|
+
title: 'Real-time Updates',
|
|
326
|
+
content: 'Data refreshed as events happen.',
|
|
327
|
+
icon: { html: '<img src=\"/icons/realtime.svg\" alt=\"\" />', width: 48, height: 48 },
|
|
288
328
|
link: { text: 'Learn more', href: '/real-time' }
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
id: 3,
|
|
332
|
+
title: 'Archive Access',
|
|
333
|
+
content: 'Query decades of historical imagery.',
|
|
334
|
+
image: '/images/archive-thumb.png',
|
|
335
|
+
link: { text: 'Explore', href: '/archive' }
|
|
336
|
+
}
|
|
337
|
+
]"
|
|
338
|
+
/>
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
###### `poster`
|
|
342
|
+
|
|
343
|
+
An image-overlay card where a background image fills the card and text is rendered on top with a dark gradient. Adds one extra prop:
|
|
344
|
+
|
|
345
|
+
| Prop | Type | Default | Description |
|
|
346
|
+
| :---------- | :------- | :-------- | :------------------------------------- |
|
|
347
|
+
| `minHeight` | `String` | `"20rem"` | Minimum CSS height of the poster card. |
|
|
348
|
+
|
|
349
|
+
```html
|
|
350
|
+
<FeaturesGallery
|
|
351
|
+
section-title="Case Studies"
|
|
352
|
+
variant="poster"
|
|
353
|
+
columns="3/2/1"
|
|
354
|
+
:cards="[
|
|
355
|
+
{
|
|
356
|
+
id: 1,
|
|
357
|
+
title: 'Arctic Monitoring',
|
|
358
|
+
content: 'Year-round ice extent tracking.',
|
|
359
|
+
image: '/images/arctic.jpg',
|
|
360
|
+
chips: [{ text: 'Featured' }],
|
|
361
|
+
metadata: [{ text: 'Jan 2024', icon: 'mdi-calendar' }],
|
|
362
|
+
link: { text: 'Read case study', href: '/arctic' },
|
|
363
|
+
secondaryLink: { text: 'Dataset', href: '/datasets/arctic' }
|
|
289
364
|
}
|
|
290
365
|
]"
|
|
291
366
|
/>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eox/pages-theme-eox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vitepress Theme with EOX branding",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@eox/eslint-config": "^2.0.0",
|
|
17
|
-
"@eox/ui": "^0.
|
|
17
|
+
"@eox/ui": "^1.0.1",
|
|
18
18
|
"vitepress": "^1.6.3"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import FeatureCard from "./Index.vue";
|
|
2
|
+
|
|
3
|
+
describe("<FeatureCard />", () => {
|
|
4
|
+
const baseProps = {
|
|
5
|
+
title: "Test Feature",
|
|
6
|
+
content: "This is a test feature content.",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
it("renders title and content using props", () => {
|
|
10
|
+
cy.mount(FeatureCard, {
|
|
11
|
+
props: baseProps,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
cy.get("h5").should("contain", baseProps.title);
|
|
15
|
+
cy.contains(baseProps.content).should("exist");
|
|
16
|
+
cy.get("article").should("have.class", "vertical");
|
|
17
|
+
cy.get("article").should("have.class", "large-padding");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders content in a paragraph when using slots", () => {
|
|
21
|
+
const slotContent = "This is content from a slot.";
|
|
22
|
+
|
|
23
|
+
cy.mount(FeatureCard, {
|
|
24
|
+
props: {
|
|
25
|
+
...baseProps,
|
|
26
|
+
content: "Fallback content",
|
|
27
|
+
},
|
|
28
|
+
slots: {
|
|
29
|
+
default: slotContent,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
cy.get("h5").should("contain", baseProps.title);
|
|
34
|
+
cy.get("p").should("contain", slotContent);
|
|
35
|
+
cy.contains("Fallback content").should("not.exist");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders primary link with explicit target", () => {
|
|
39
|
+
const link = {
|
|
40
|
+
text: "Learn More",
|
|
41
|
+
href: "https://example.com",
|
|
42
|
+
target: "_blank",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
cy.mount(FeatureCard, {
|
|
46
|
+
props: {
|
|
47
|
+
...baseProps,
|
|
48
|
+
link,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
cy.get("nav").should("exist");
|
|
53
|
+
cy.contains("a", "Learn More")
|
|
54
|
+
.should("have.attr", "href", "https://example.com")
|
|
55
|
+
.and("have.attr", "target", "_blank");
|
|
56
|
+
cy.get(".arrow").should("exist");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("defaults primary link target to _blank", () => {
|
|
60
|
+
cy.mount(FeatureCard, {
|
|
61
|
+
props: {
|
|
62
|
+
...baseProps,
|
|
63
|
+
link: {
|
|
64
|
+
text: "Read",
|
|
65
|
+
href: "/docs",
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
cy.contains("a", "Read")
|
|
71
|
+
.should("have.attr", "target", "_blank")
|
|
72
|
+
.and("have.attr", "href", "/docs");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("renders secondary link and nav layout when provided", () => {
|
|
76
|
+
cy.mount(FeatureCard, {
|
|
77
|
+
props: {
|
|
78
|
+
...baseProps,
|
|
79
|
+
secondaryLink: {
|
|
80
|
+
text: "Docs",
|
|
81
|
+
href: "/docs",
|
|
82
|
+
target: "_self",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
cy.get("nav").should("exist");
|
|
88
|
+
cy.get(".secondary-link")
|
|
89
|
+
.should("contain", "Docs")
|
|
90
|
+
.and("have.attr", "href", "/docs")
|
|
91
|
+
.and("have.attr", "target", "_self");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("renders icon from class string", () => {
|
|
95
|
+
cy.mount(FeatureCard, {
|
|
96
|
+
props: {
|
|
97
|
+
...baseProps,
|
|
98
|
+
icon: "mdi-home",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
cy.get("i.mdi.mdi-home").should("exist");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("renders html icon", () => {
|
|
106
|
+
const iconHtml = '<svg><circle cx="50" cy="50" r="40" /></svg>';
|
|
107
|
+
cy.mount(FeatureCard, {
|
|
108
|
+
props: {
|
|
109
|
+
...baseProps,
|
|
110
|
+
icon: { html: iconHtml },
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
cy.get(".icon").should("exist");
|
|
115
|
+
cy.get(".icon").find("svg").should("exist");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders image fallback when no icon is provided", () => {
|
|
119
|
+
cy.mount(FeatureCard, {
|
|
120
|
+
props: {
|
|
121
|
+
...baseProps,
|
|
122
|
+
image: "/feature.png",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
cy.get("img.icon").should("have.attr", "src", "/feature.png");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("renders chips and metadata", () => {
|
|
130
|
+
cy.mount(FeatureCard, {
|
|
131
|
+
props: {
|
|
132
|
+
...baseProps,
|
|
133
|
+
chips: [{ text: "New", class: "primary" }, { text: "Popular" }],
|
|
134
|
+
metadata: [
|
|
135
|
+
{ text: "5 min read", icon: "mdi-clock-outline" },
|
|
136
|
+
{ text: "Updated weekly" },
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
cy.get(".small-chip").should("have.length", 2);
|
|
142
|
+
cy.contains(".small-chip", "New").should("have.class", "primary");
|
|
143
|
+
cy.contains(".small-chip", "Popular").should("exist");
|
|
144
|
+
cy.get(".metadata-item").should("have.length", 2);
|
|
145
|
+
cy.get(".metadata-item .mdi-clock-outline").should("exist");
|
|
146
|
+
cy.contains("Updated weekly").should("exist");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("does not render nav when no links are provided", () => {
|
|
150
|
+
cy.mount(FeatureCard, {
|
|
151
|
+
props: baseProps,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
cy.get("nav").should("not.exist");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const { title, content, link, secondaryLink, icon } = defineProps({
|
|
3
|
+
title: String,
|
|
4
|
+
content: String,
|
|
5
|
+
image: {
|
|
6
|
+
type: String,
|
|
7
|
+
required: false,
|
|
8
|
+
},
|
|
9
|
+
chips: {
|
|
10
|
+
/** @type {import('vue').PropType<{ text: string; class?: string }[]>} */
|
|
11
|
+
type: Array,
|
|
12
|
+
required: false,
|
|
13
|
+
default: () => [],
|
|
14
|
+
},
|
|
15
|
+
metadata: {
|
|
16
|
+
/** @type {import('vue').PropType<{ text: string; icon?: string }[]>} */
|
|
17
|
+
type: Array,
|
|
18
|
+
required: false,
|
|
19
|
+
default: () => [],
|
|
20
|
+
},
|
|
21
|
+
link: {
|
|
22
|
+
/** @type {import('vue').PropType<{ text:string ,href: string,target?: string }>} */
|
|
23
|
+
type: Object,
|
|
24
|
+
required: false,
|
|
25
|
+
},
|
|
26
|
+
secondaryLink: {
|
|
27
|
+
/** @type {import('vue').PropType<{ text:string ,href: string,target?: string }>} */
|
|
28
|
+
type: Object,
|
|
29
|
+
required: false,
|
|
30
|
+
},
|
|
31
|
+
icon: {
|
|
32
|
+
/** @type {import('vue').PropType<{ html: string, width?: number, height?: number }>} */
|
|
33
|
+
type: Object,
|
|
34
|
+
required: false,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const iconStyle = {
|
|
38
|
+
width: (icon?.width ?? 40) + "px",
|
|
39
|
+
height: (icon?.height ?? 40) + "px",
|
|
40
|
+
};
|
|
41
|
+
</script>
|
|
42
|
+
<template>
|
|
43
|
+
<article class="vertical large-padding">
|
|
44
|
+
<div>
|
|
45
|
+
<i
|
|
46
|
+
v-if="typeof icon === 'string' && icon.startsWith('mdi-')"
|
|
47
|
+
:class="`mdi ${icon}`"
|
|
48
|
+
></i>
|
|
49
|
+
<div
|
|
50
|
+
v-else-if="icon"
|
|
51
|
+
:style="iconStyle"
|
|
52
|
+
v-html="icon.html"
|
|
53
|
+
class="icon"
|
|
54
|
+
></div>
|
|
55
|
+
<img v-else-if="image" :src="image" alt="" class="icon" />
|
|
56
|
+
<h5 class="small">{{ title }}</h5>
|
|
57
|
+
<div
|
|
58
|
+
v-if="chips && chips.length"
|
|
59
|
+
class="row wrap"
|
|
60
|
+
style="gap: 0.5rem; margin-bottom: 0.5rem"
|
|
61
|
+
>
|
|
62
|
+
<span
|
|
63
|
+
v-for="chip in chips"
|
|
64
|
+
:key="chip.text"
|
|
65
|
+
class="chip small-chip border"
|
|
66
|
+
:class="chip.class"
|
|
67
|
+
>
|
|
68
|
+
{{ chip.text }}
|
|
69
|
+
</span>
|
|
70
|
+
</div>
|
|
71
|
+
<p v-if="metadata && metadata.length" class="small-text">
|
|
72
|
+
<span
|
|
73
|
+
v-for="(item, index) in metadata"
|
|
74
|
+
:key="index"
|
|
75
|
+
class="metadata-item"
|
|
76
|
+
>
|
|
77
|
+
<i
|
|
78
|
+
v-if="item.icon"
|
|
79
|
+
:class="`small mdi ${item.icon}`"
|
|
80
|
+
style="transform: translateY(-1px)"
|
|
81
|
+
></i>
|
|
82
|
+
{{ item.text }}
|
|
83
|
+
<br v-if="index < metadata.length - 1" />
|
|
84
|
+
</span>
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<p v-if="$slots.default">
|
|
88
|
+
<slot>{{ content }}</slot>
|
|
89
|
+
</p>
|
|
90
|
+
<div v-else v-html="content"></div>
|
|
91
|
+
</div>
|
|
92
|
+
<nav
|
|
93
|
+
v-if="link || secondaryLink"
|
|
94
|
+
class="row wrap align-center"
|
|
95
|
+
style="gap: 1.5rem; margin-right: auto"
|
|
96
|
+
>
|
|
97
|
+
<a
|
|
98
|
+
v-if="link"
|
|
99
|
+
:href="link.href"
|
|
100
|
+
:target="link.target ?? '_blank'"
|
|
101
|
+
class="button transparent bold primary-text no-padding"
|
|
102
|
+
>
|
|
103
|
+
<span>
|
|
104
|
+
{{ link.text }}
|
|
105
|
+
</span>
|
|
106
|
+
<i class="mdi mdi-chevron-right arrow"></i>
|
|
107
|
+
</a>
|
|
108
|
+
<a
|
|
109
|
+
v-if="secondaryLink"
|
|
110
|
+
:href="secondaryLink.href"
|
|
111
|
+
:target="secondaryLink.target ?? '_blank'"
|
|
112
|
+
class="button transparent primary-text no-padding secondary-link"
|
|
113
|
+
>
|
|
114
|
+
<span>
|
|
115
|
+
{{ secondaryLink.text }}
|
|
116
|
+
</span>
|
|
117
|
+
</a>
|
|
118
|
+
</nav>
|
|
119
|
+
</article>
|
|
120
|
+
</template>
|
|
121
|
+
<style scoped>
|
|
122
|
+
/**
|
|
123
|
+
* Undo Vitepress missing with @eox/ui styles
|
|
124
|
+
*/
|
|
125
|
+
.VPHome .vp-doc a.button.bold {
|
|
126
|
+
font-weight: bold;
|
|
127
|
+
}
|
|
128
|
+
.secondary-link {
|
|
129
|
+
font-weight: normal;
|
|
130
|
+
opacity: 0.8;
|
|
131
|
+
}
|
|
132
|
+
.secondary-link:hover {
|
|
133
|
+
opacity: 1;
|
|
134
|
+
}
|
|
135
|
+
.VPHome .vp-doc h5.small {
|
|
136
|
+
font-size: 1.25rem;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Custom styles
|
|
141
|
+
*/
|
|
142
|
+
article {
|
|
143
|
+
justify-content: space-between;
|
|
144
|
+
transition: all 0.3s ease-in-out;
|
|
145
|
+
width: 100%;
|
|
146
|
+
}
|
|
147
|
+
.small-chip {
|
|
148
|
+
font-size: 0.75rem;
|
|
149
|
+
padding: 0.125rem 0.375rem;
|
|
150
|
+
height: auto;
|
|
151
|
+
line-height: normal;
|
|
152
|
+
margin: 0;
|
|
153
|
+
}
|
|
154
|
+
.arrow {
|
|
155
|
+
transition: transform 0.2s;
|
|
156
|
+
}
|
|
157
|
+
.button:hover .arrow {
|
|
158
|
+
transform: translateX(4px);
|
|
159
|
+
}
|
|
160
|
+
.button.transparent:hover:after {
|
|
161
|
+
background: none;
|
|
162
|
+
}
|
|
163
|
+
</style>
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import PosterCard from "./Poster.vue";
|
|
2
|
+
|
|
3
|
+
describe("<FeatureCard Poster />", () => {
|
|
4
|
+
const baseProps = {
|
|
5
|
+
title: "Poster Feature",
|
|
6
|
+
content: "Poster content",
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
it("renders title and content using props", () => {
|
|
10
|
+
cy.mount(PosterCard, {
|
|
11
|
+
props: baseProps,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
cy.get("article.image-overlay-card").should("exist");
|
|
15
|
+
cy.get("h5").should("contain", baseProps.title);
|
|
16
|
+
cy.contains(baseProps.content).should("exist");
|
|
17
|
+
cy.get(".image-overlay-card-overlay").should("exist");
|
|
18
|
+
cy.get(".image-overlay-card-content").should("exist");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("renders slot content over content prop when slot exists", () => {
|
|
22
|
+
const slotContent = "This is content from a slot.";
|
|
23
|
+
|
|
24
|
+
cy.mount(PosterCard, {
|
|
25
|
+
props: {
|
|
26
|
+
...baseProps,
|
|
27
|
+
content: "Fallback content",
|
|
28
|
+
},
|
|
29
|
+
slots: {
|
|
30
|
+
default: slotContent,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
cy.get("p").should("contain", slotContent);
|
|
35
|
+
cy.contains("Fallback content").should("not.exist");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("renders poster image when image prop is set", () => {
|
|
39
|
+
cy.mount(PosterCard, {
|
|
40
|
+
props: {
|
|
41
|
+
...baseProps,
|
|
42
|
+
image: "/poster.png",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
cy.get("img.image-overlay-card-image").should(
|
|
47
|
+
"have.attr",
|
|
48
|
+
"src",
|
|
49
|
+
"/poster.png",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("applies custom minHeight", () => {
|
|
54
|
+
cy.mount(PosterCard, {
|
|
55
|
+
props: {
|
|
56
|
+
...baseProps,
|
|
57
|
+
minHeight: "28rem",
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
cy.get("article.image-overlay-card")
|
|
62
|
+
.should("have.attr", "style")
|
|
63
|
+
.and("include", "min-height: 28rem");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("renders primary and secondary links with targets", () => {
|
|
67
|
+
const link = {
|
|
68
|
+
text: "Learn More",
|
|
69
|
+
href: "https://example.com",
|
|
70
|
+
target: "_blank",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const secondaryLink = {
|
|
74
|
+
text: "Docs",
|
|
75
|
+
href: "/docs",
|
|
76
|
+
target: "_self",
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
cy.mount(PosterCard, {
|
|
80
|
+
props: {
|
|
81
|
+
...baseProps,
|
|
82
|
+
link,
|
|
83
|
+
secondaryLink,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
cy.get("nav").should("exist");
|
|
88
|
+
cy.contains("a", "Learn More")
|
|
89
|
+
.should("have.attr", "href", "https://example.com")
|
|
90
|
+
.and("have.attr", "target", "_blank");
|
|
91
|
+
cy.contains("a", "Docs")
|
|
92
|
+
.should("have.attr", "href", "/docs")
|
|
93
|
+
.and("have.attr", "target", "_self");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("defaults link target to _blank when omitted", () => {
|
|
97
|
+
cy.mount(PosterCard, {
|
|
98
|
+
props: {
|
|
99
|
+
...baseProps,
|
|
100
|
+
link: {
|
|
101
|
+
text: "Read",
|
|
102
|
+
href: "/read",
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
cy.contains("a", "Read")
|
|
108
|
+
.should("have.attr", "href", "/read")
|
|
109
|
+
.and("have.attr", "target", "_blank");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("renders chips and metadata", () => {
|
|
113
|
+
cy.mount(PosterCard, {
|
|
114
|
+
props: {
|
|
115
|
+
...baseProps,
|
|
116
|
+
chips: [{ text: "Launch" }, { text: "EOX", class: "primary" }],
|
|
117
|
+
metadata: [
|
|
118
|
+
{ text: "10 min", icon: "mdi-clock-outline" },
|
|
119
|
+
{ text: "Advanced" },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
cy.get(".small-chip").should("have.length", 2);
|
|
125
|
+
cy.contains(".small-chip", "EOX").should("have.class", "primary");
|
|
126
|
+
cy.get(".metadata-item").should("have.length", 2);
|
|
127
|
+
cy.get(".metadata-item .mdi-clock-outline").should("exist");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("does not render nav when links are not provided", () => {
|
|
131
|
+
cy.mount(PosterCard, {
|
|
132
|
+
props: baseProps,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
cy.get("nav").should("not.exist");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import { computed } from "vue";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
title: String,
|
|
6
|
+
content: String,
|
|
7
|
+
link: {
|
|
8
|
+
/** @type {import('vue').PropType<{ text:string ,href: string,target?: string }>} */
|
|
9
|
+
type: Object,
|
|
10
|
+
required: false,
|
|
11
|
+
},
|
|
12
|
+
icon: {
|
|
13
|
+
/** @type {import('vue').PropType<string | { html: string, width?: number, height?: number }>} */
|
|
14
|
+
type: [String, Object],
|
|
15
|
+
required: false,
|
|
16
|
+
},
|
|
17
|
+
image: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: false,
|
|
20
|
+
},
|
|
21
|
+
chips: {
|
|
22
|
+
/** @type {import('vue').PropType<{ text: string; class?: string }[]>} */
|
|
23
|
+
type: Array,
|
|
24
|
+
required: false,
|
|
25
|
+
default: () => [],
|
|
26
|
+
},
|
|
27
|
+
metadata: {
|
|
28
|
+
/** @type {import('vue').PropType<{ text: string; icon?: string }[]>} */
|
|
29
|
+
type: Array,
|
|
30
|
+
required: false,
|
|
31
|
+
default: () => [],
|
|
32
|
+
},
|
|
33
|
+
secondaryLink: {
|
|
34
|
+
/** @type {import('vue').PropType<{ text: string, href: string, target?: string }>} */
|
|
35
|
+
type: Object,
|
|
36
|
+
required: false,
|
|
37
|
+
},
|
|
38
|
+
minHeight: {
|
|
39
|
+
type: String,
|
|
40
|
+
default: "20rem",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const cardStyle = computed(() => ({
|
|
45
|
+
minHeight: props.minHeight,
|
|
46
|
+
}));
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<template>
|
|
50
|
+
<article
|
|
51
|
+
class="no-padding fill small-elevate image-overlay-card"
|
|
52
|
+
:style="cardStyle"
|
|
53
|
+
>
|
|
54
|
+
<img v-if="image" :src="image" alt="" class="image-overlay-card-image" />
|
|
55
|
+
<div class="image-overlay-card-overlay"></div>
|
|
56
|
+
<div class="image-overlay-card-content padding">
|
|
57
|
+
<div style="padding-top: 3rem">
|
|
58
|
+
<div
|
|
59
|
+
v-if="chips && chips.length"
|
|
60
|
+
class="row wrap absolute top"
|
|
61
|
+
style="gap: 0.5rem; margin-top: 1.5rem"
|
|
62
|
+
>
|
|
63
|
+
<span
|
|
64
|
+
v-for="chip in chips"
|
|
65
|
+
:key="chip.text"
|
|
66
|
+
class="chip small-chip border white-text"
|
|
67
|
+
:class="chip.class"
|
|
68
|
+
>
|
|
69
|
+
{{ chip.text }}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
72
|
+
<h5 class="small white-text">{{ title }}</h5>
|
|
73
|
+
<p v-if="metadata && metadata.length" class="small-text white-text">
|
|
74
|
+
<span
|
|
75
|
+
v-for="(item, index) in metadata"
|
|
76
|
+
:key="index"
|
|
77
|
+
class="metadata-item"
|
|
78
|
+
>
|
|
79
|
+
<i
|
|
80
|
+
v-if="item.icon"
|
|
81
|
+
:class="`small mdi ${item.icon}`"
|
|
82
|
+
style="transform: translateY(-1px)"
|
|
83
|
+
></i>
|
|
84
|
+
{{ item.text }}
|
|
85
|
+
<br v-if="index < metadata.length - 1" />
|
|
86
|
+
</span>
|
|
87
|
+
</p>
|
|
88
|
+
<p v-if="$slots.default" class="medium-text white-text">
|
|
89
|
+
<slot>{{ content }}</slot>
|
|
90
|
+
</p>
|
|
91
|
+
<p
|
|
92
|
+
v-else-if="content"
|
|
93
|
+
class="medium-text white-text"
|
|
94
|
+
v-html="content"
|
|
95
|
+
></p>
|
|
96
|
+
</div>
|
|
97
|
+
<nav
|
|
98
|
+
v-if="link || secondaryLink"
|
|
99
|
+
class="row wrap align-center"
|
|
100
|
+
style="gap: 0.5rem"
|
|
101
|
+
>
|
|
102
|
+
<a
|
|
103
|
+
v-if="link"
|
|
104
|
+
class="button small white black-text"
|
|
105
|
+
:href="link.href"
|
|
106
|
+
:target="link.target ?? '_blank'"
|
|
107
|
+
>
|
|
108
|
+
{{ link.text }}
|
|
109
|
+
</a>
|
|
110
|
+
<a
|
|
111
|
+
v-if="secondaryLink"
|
|
112
|
+
class="button small border white-text secondary-link"
|
|
113
|
+
:href="secondaryLink.href"
|
|
114
|
+
:target="secondaryLink.target ?? '_blank'"
|
|
115
|
+
>
|
|
116
|
+
{{ secondaryLink.text }}
|
|
117
|
+
</a>
|
|
118
|
+
</nav>
|
|
119
|
+
</div>
|
|
120
|
+
</article>
|
|
121
|
+
</template>
|
|
122
|
+
|
|
123
|
+
<style scoped>
|
|
124
|
+
.image-overlay-card {
|
|
125
|
+
position: relative;
|
|
126
|
+
display: flex;
|
|
127
|
+
flex-direction: column;
|
|
128
|
+
transition:
|
|
129
|
+
transform 0.3s ease,
|
|
130
|
+
box-shadow 0.3s ease;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.image-overlay-card-image {
|
|
135
|
+
position: absolute;
|
|
136
|
+
inset: 0;
|
|
137
|
+
width: 100%;
|
|
138
|
+
height: 100%;
|
|
139
|
+
object-fit: cover;
|
|
140
|
+
z-index: 0;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.image-overlay-card-overlay {
|
|
144
|
+
position: absolute;
|
|
145
|
+
inset: 0;
|
|
146
|
+
background: linear-gradient(
|
|
147
|
+
to bottom,
|
|
148
|
+
rgba(0, 0, 0, 0.1) 0%,
|
|
149
|
+
rgba(0, 0, 0, 0.6) 40%,
|
|
150
|
+
rgba(0, 0, 0, 0.9) 100%
|
|
151
|
+
);
|
|
152
|
+
border-radius: inherit;
|
|
153
|
+
z-index: 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.image-overlay-card-content {
|
|
157
|
+
position: relative;
|
|
158
|
+
z-index: 2;
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
justify-content: flex-end;
|
|
162
|
+
flex: 1;
|
|
163
|
+
}
|
|
164
|
+
.small-chip {
|
|
165
|
+
font-size: 0.75rem;
|
|
166
|
+
padding: 0.125rem 0.375rem;
|
|
167
|
+
height: auto;
|
|
168
|
+
line-height: normal;
|
|
169
|
+
margin: 0;
|
|
170
|
+
pointer-events: none;
|
|
171
|
+
}
|
|
172
|
+
</style>
|
|
@@ -3,8 +3,23 @@ import { __setMockData } from "../../cypress/support/mocks/vitepress";
|
|
|
3
3
|
|
|
4
4
|
describe("<FeaturesGallery />", () => {
|
|
5
5
|
const cards = [
|
|
6
|
-
{
|
|
7
|
-
|
|
6
|
+
{
|
|
7
|
+
id: 1,
|
|
8
|
+
title: "Card 1",
|
|
9
|
+
content: "Content 1",
|
|
10
|
+
icon: "mdi-home",
|
|
11
|
+
chips: [{ text: "New", class: "primary" }],
|
|
12
|
+
metadata: [{ text: "5 min", icon: "mdi-clock-outline" }],
|
|
13
|
+
link: { text: "Read more", href: "/feature-1", target: "_self" },
|
|
14
|
+
secondaryLink: { text: "Docs", href: "/docs", target: "_self" },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: 2,
|
|
18
|
+
title: "Card 2",
|
|
19
|
+
content: "Content 2",
|
|
20
|
+
image: "/image.png",
|
|
21
|
+
link: { href: "/card-2", target: "_self" },
|
|
22
|
+
},
|
|
8
23
|
];
|
|
9
24
|
|
|
10
25
|
beforeEach(() => {
|
|
@@ -22,6 +37,17 @@ describe("<FeaturesGallery />", () => {
|
|
|
22
37
|
cy.contains("h5", "My Gallery").should("be.visible");
|
|
23
38
|
});
|
|
24
39
|
|
|
40
|
+
it("hides section title when sectionTitle is false", () => {
|
|
41
|
+
cy.mount(FeaturesGallery, {
|
|
42
|
+
props: {
|
|
43
|
+
cards,
|
|
44
|
+
sectionTitle: false,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
cy.get(".holder > h5").should("not.exist");
|
|
49
|
+
});
|
|
50
|
+
|
|
25
51
|
it("renders feature cards", () => {
|
|
26
52
|
cy.mount(FeaturesGallery, {
|
|
27
53
|
props: { cards },
|
|
@@ -30,16 +56,100 @@ describe("<FeaturesGallery />", () => {
|
|
|
30
56
|
cy.contains("Card 1").should("exist");
|
|
31
57
|
cy.contains("Content 1").should("exist");
|
|
32
58
|
cy.contains("Card 2").should("exist");
|
|
59
|
+
cy.get(".card-wrapper").should("have.length", 2);
|
|
33
60
|
});
|
|
34
61
|
|
|
35
|
-
it("applies background classes", () => {
|
|
62
|
+
it("applies background and card background classes", () => {
|
|
36
63
|
cy.mount(FeaturesGallery, {
|
|
37
64
|
props: {
|
|
38
65
|
cards,
|
|
39
66
|
background: "custom-bg-class",
|
|
67
|
+
cardBackground: "surface-container",
|
|
40
68
|
},
|
|
41
69
|
});
|
|
42
70
|
|
|
43
|
-
cy.get("section")
|
|
71
|
+
cy.get("section")
|
|
72
|
+
.should("have.class", "custom-bg-class")
|
|
73
|
+
.and("have.class", "full-width");
|
|
74
|
+
cy.get(".card-wrapper article")
|
|
75
|
+
.first()
|
|
76
|
+
.should("have.class", "surface-container");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("uses provided columns to set css variables", () => {
|
|
80
|
+
cy.mount(FeaturesGallery, {
|
|
81
|
+
props: {
|
|
82
|
+
cards: [cards[0]],
|
|
83
|
+
columns: "3/2/1",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
cy.get(".cards-gallery")
|
|
88
|
+
.should("have.attr", "style")
|
|
89
|
+
.and("include", "--cols-l: 3")
|
|
90
|
+
.and("include", "--cols-m: 2")
|
|
91
|
+
.and("include", "--cols-s: 1");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("wraps cards in anchor when link has no text", () => {
|
|
95
|
+
cy.mount(FeaturesGallery, {
|
|
96
|
+
props: {
|
|
97
|
+
cards: [cards[1]],
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
cy.get("a.link-card")
|
|
102
|
+
.should("have.attr", "href", "/card-2")
|
|
103
|
+
.and("have.attr", "target", "_self");
|
|
104
|
+
cy.get("a.link-card article").should("exist");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("does not wrap card when link has text", () => {
|
|
108
|
+
cy.mount(FeaturesGallery, {
|
|
109
|
+
props: {
|
|
110
|
+
cards: [cards[0]],
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
cy.get("a.link-card").should("not.exist");
|
|
115
|
+
cy.contains("a", "Read more")
|
|
116
|
+
.should("have.attr", "href", "/feature-1")
|
|
117
|
+
.and("have.attr", "target", "_self");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("renders poster variant when selected", () => {
|
|
121
|
+
cy.mount(FeaturesGallery, {
|
|
122
|
+
props: {
|
|
123
|
+
cards: [cards[0]],
|
|
124
|
+
variant: "poster",
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
cy.get("article.image-overlay-card").should("exist");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("falls back to default variant for unknown variant", () => {
|
|
132
|
+
cy.mount(FeaturesGallery, {
|
|
133
|
+
props: {
|
|
134
|
+
cards: [cards[0]],
|
|
135
|
+
variant: "does-not-exist",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
cy.get("article.image-overlay-card").should("not.exist");
|
|
140
|
+
cy.get("article.vertical.large-padding").should("exist");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("renders cards from mocked features data when cards prop is missing", () => {
|
|
144
|
+
__setMockData({
|
|
145
|
+
page: {
|
|
146
|
+
relativePath: "index.md",
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
cy.mount(FeaturesGallery);
|
|
151
|
+
|
|
152
|
+
cy.get(".card-wrapper").should("have.length.at.least", 1);
|
|
153
|
+
cy.contains("Read more").should("exist");
|
|
44
154
|
});
|
|
45
155
|
});
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { useData, withBase } from "vitepress";
|
|
3
3
|
import { data as features } from "../features.data.js";
|
|
4
|
-
import
|
|
4
|
+
import FeatureCardDefault from "./FeatureCard/Index.vue";
|
|
5
|
+
import { defineAsyncComponent, computed } from "vue";
|
|
5
6
|
|
|
6
|
-
const { cards, sectionTitle, background } = defineProps({
|
|
7
|
+
const { cards, sectionTitle, background, variant, columns } = defineProps({
|
|
7
8
|
sectionTitle: {
|
|
8
|
-
type: String,
|
|
9
|
+
type: [String, Boolean],
|
|
10
|
+
},
|
|
11
|
+
variant: {
|
|
12
|
+
type: /** @type {import('vue').PropType<keyof typeof cardComponents>} */ (
|
|
13
|
+
String
|
|
14
|
+
),
|
|
15
|
+
default: "default",
|
|
16
|
+
},
|
|
17
|
+
columns: {
|
|
18
|
+
type: [String, Number],
|
|
19
|
+
default: "4/2/1",
|
|
9
20
|
},
|
|
10
21
|
cards: {
|
|
11
22
|
/**
|
|
@@ -13,8 +24,12 @@ const { cards, sectionTitle, background } = defineProps({
|
|
|
13
24
|
id: number | string,
|
|
14
25
|
title: string,
|
|
15
26
|
content: string,
|
|
16
|
-
|
|
17
|
-
|
|
27
|
+
image?: string,
|
|
28
|
+
chips?: { text: string; class?: string }[],
|
|
29
|
+
metadata?: { text: string; icon?: string }[],
|
|
30
|
+
link?: { text: string, href: string,target?: string },
|
|
31
|
+
secondaryLink?: { text: string, href: string,target?: string },
|
|
32
|
+
icon?: string | {
|
|
18
33
|
html: string,
|
|
19
34
|
width?: number,
|
|
20
35
|
height?: number,
|
|
@@ -33,6 +48,19 @@ const { cards, sectionTitle, background } = defineProps({
|
|
|
33
48
|
},
|
|
34
49
|
});
|
|
35
50
|
|
|
51
|
+
const cardComponents = {
|
|
52
|
+
default: FeatureCardDefault,
|
|
53
|
+
poster: defineAsyncComponent(() => import("./FeatureCard/Poster.vue")),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** @param {keyof typeof cardComponents | undefined} variant */
|
|
57
|
+
const resolveCardComponent = (variant) => {
|
|
58
|
+
if (!variant) {
|
|
59
|
+
return FeatureCardDefault;
|
|
60
|
+
}
|
|
61
|
+
return cardComponents[variant] || FeatureCardDefault;
|
|
62
|
+
};
|
|
63
|
+
|
|
36
64
|
const { page, site } = useData();
|
|
37
65
|
|
|
38
66
|
const featuresExcerpts =
|
|
@@ -73,6 +101,19 @@ const siteTitle =
|
|
|
73
101
|
sectionTitle !== false
|
|
74
102
|
? sectionTitle || `More ${site.value.title} features:`
|
|
75
103
|
: false;
|
|
104
|
+
|
|
105
|
+
const gridCols = computed(() => {
|
|
106
|
+
let colsStr = String(columns || "4/2/1");
|
|
107
|
+
if (!colsStr.includes("/")) {
|
|
108
|
+
colsStr = `${colsStr}/2/1`;
|
|
109
|
+
}
|
|
110
|
+
const [l, m, s] = colsStr.split("/");
|
|
111
|
+
return {
|
|
112
|
+
"--cols-l": l || 4,
|
|
113
|
+
"--cols-m": m || 2,
|
|
114
|
+
"--cols-s": s || 1,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
76
117
|
</script>
|
|
77
118
|
|
|
78
119
|
<template>
|
|
@@ -82,31 +123,42 @@ const siteTitle =
|
|
|
82
123
|
<h5>{{ siteTitle }}</h5>
|
|
83
124
|
<div class="medium-space"></div>
|
|
84
125
|
</template>
|
|
85
|
-
<div class="cards-gallery">
|
|
126
|
+
<div class="cards-gallery" :style="gridCols">
|
|
86
127
|
<div
|
|
87
128
|
v-for="feature in featuresExcerpts"
|
|
88
129
|
:key="feature.id"
|
|
89
130
|
class="card-wrapper"
|
|
90
131
|
>
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
<
|
|
132
|
+
<div v-if="feature.link && !feature.link.text" class="link-card">
|
|
133
|
+
<a
|
|
134
|
+
:href="feature.link.href"
|
|
135
|
+
:target="feature.link.target"
|
|
136
|
+
class="link-overlay"
|
|
137
|
+
></a>
|
|
138
|
+
<component
|
|
139
|
+
:is="resolveCardComponent(variant)"
|
|
98
140
|
:title="feature.title"
|
|
99
141
|
:content="feature.content"
|
|
100
142
|
:icon="feature.icon"
|
|
143
|
+
:image="feature.image"
|
|
144
|
+
:chips="feature.chips"
|
|
145
|
+
:metadata="feature.metadata"
|
|
146
|
+
:link="feature.link"
|
|
147
|
+
:secondaryLink="feature.secondaryLink"
|
|
101
148
|
:class="`${cardBackground}`"
|
|
102
149
|
/>
|
|
103
|
-
</
|
|
104
|
-
<
|
|
150
|
+
</div>
|
|
151
|
+
<component
|
|
105
152
|
v-else
|
|
153
|
+
:is="resolveCardComponent(variant)"
|
|
106
154
|
:title="feature.title"
|
|
107
155
|
:content="feature.content"
|
|
108
156
|
:icon="feature.icon"
|
|
109
|
-
:
|
|
157
|
+
:image="feature.image"
|
|
158
|
+
:chips="feature.chips"
|
|
159
|
+
:metadata="feature.metadata"
|
|
160
|
+
:link="feature.link"
|
|
161
|
+
:secondaryLink="feature.secondaryLink"
|
|
110
162
|
:class="`${cardBackground}`"
|
|
111
163
|
/>
|
|
112
164
|
</div>
|
|
@@ -130,6 +182,12 @@ section {
|
|
|
130
182
|
display: block;
|
|
131
183
|
text-decoration: none;
|
|
132
184
|
border: 0.0625rem solid transparent;
|
|
185
|
+
position: relative;
|
|
186
|
+
}
|
|
187
|
+
.link-overlay {
|
|
188
|
+
position: absolute;
|
|
189
|
+
inset: 0;
|
|
190
|
+
z-index: 1;
|
|
133
191
|
}
|
|
134
192
|
.link-card:hover article {
|
|
135
193
|
box-shadow: none;
|
|
@@ -138,24 +196,19 @@ section {
|
|
|
138
196
|
|
|
139
197
|
.card-wrapper {
|
|
140
198
|
display: flex;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@media (max-width: 1280px) {
|
|
145
|
-
.card-wrapper {
|
|
146
|
-
flex: 0 0 calc(33.333% - 16px);
|
|
147
|
-
}
|
|
199
|
+
--cols: var(--cols-l, 4);
|
|
200
|
+
flex: 0 0 calc(100% / var(--cols) - (24px * (var(--cols) - 1) / var(--cols)));
|
|
148
201
|
}
|
|
149
202
|
|
|
150
203
|
@media (max-width: 960px) {
|
|
151
204
|
.card-wrapper {
|
|
152
|
-
|
|
205
|
+
--cols: var(--cols-m, 2);
|
|
153
206
|
}
|
|
154
207
|
}
|
|
155
208
|
|
|
156
209
|
@media (max-width: 640px) {
|
|
157
210
|
.card-wrapper {
|
|
158
|
-
|
|
211
|
+
--cols: var(--cols-s, 1);
|
|
159
212
|
}
|
|
160
213
|
}
|
|
161
214
|
</style>
|
|
@@ -136,17 +136,18 @@
|
|
|
136
136
|
<nav>
|
|
137
137
|
<ul class="left-align no-margin">
|
|
138
138
|
<li v-for="item in theme.nav.filter((i) => !i.action)">
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
<div v-if="item.items">
|
|
140
|
+
<button
|
|
141
|
+
class="button text"
|
|
142
|
+
:class="{ active: isActive(item, route.path) }"
|
|
143
|
+
>
|
|
144
|
+
<span>{{ t(item.text, theme.i18n) }}</span>
|
|
145
|
+
<i class="mdi mdi-chevron-down"></i>
|
|
146
|
+
</button>
|
|
146
147
|
<menu class="no-wrap surface-container-lowest">
|
|
147
148
|
<NavDropdown :items="item.items" />
|
|
148
149
|
</menu>
|
|
149
|
-
</
|
|
150
|
+
</div>
|
|
150
151
|
<a
|
|
151
152
|
v-else
|
|
152
153
|
class="button text"
|
|
@@ -197,9 +198,11 @@
|
|
|
197
198
|
</ul>
|
|
198
199
|
<ul class="left-align no-margin" v-if="langs && langs.length > 1">
|
|
199
200
|
<li>
|
|
200
|
-
<
|
|
201
|
-
<
|
|
202
|
-
|
|
201
|
+
<div>
|
|
202
|
+
<button class="button text">
|
|
203
|
+
<i class="mdi mdi-translate small"></i>
|
|
204
|
+
<i class="mdi mdi-chevron-down small"></i>
|
|
205
|
+
</button>
|
|
203
206
|
<menu class="no-wrap surface-container-lowest">
|
|
204
207
|
<li
|
|
205
208
|
v-for="lang in langs"
|
|
@@ -211,7 +214,7 @@
|
|
|
211
214
|
</a>
|
|
212
215
|
</li>
|
|
213
216
|
</menu>
|
|
214
|
-
</
|
|
217
|
+
</div>
|
|
215
218
|
</li>
|
|
216
219
|
</ul>
|
|
217
220
|
</nav>
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
@change="updatePrices"
|
|
20
20
|
/>
|
|
21
21
|
<span class="margin" :for="option.id"
|
|
22
|
-
><
|
|
22
|
+
><span :class="`bold ${isMobile ? 'medium-text' : ''}`">
|
|
23
23
|
+ {{ option.label }}
|
|
24
|
-
</
|
|
24
|
+
</span></span
|
|
25
25
|
>
|
|
26
26
|
</label>
|
|
27
27
|
</div>
|
|
@@ -122,9 +122,9 @@
|
|
|
122
122
|
@change="updatePrices"
|
|
123
123
|
/>
|
|
124
124
|
<span :for="`${plan.name}-${row}`"
|
|
125
|
-
><
|
|
125
|
+
><span class="small bold">
|
|
126
126
|
{{ plan.details[row].alternative.name }}
|
|
127
|
-
</
|
|
127
|
+
</span></span
|
|
128
128
|
>
|
|
129
129
|
</label>
|
|
130
130
|
<span
|
package/src/vitepressConfig.mjs
CHANGED
|
@@ -109,7 +109,7 @@ export const generate = (brandConfig) => {
|
|
|
109
109
|
Powered by
|
|
110
110
|
<a href="https://hub.eox.at" target="_blank" class="left-margin small-margin"
|
|
111
111
|
><img
|
|
112
|
-
src="https://hub.eox.at/
|
|
112
|
+
src="https://hub-brands.eox.at/eoxhub/eoxhub.svg"
|
|
113
113
|
style="height: 25px" /></a
|
|
114
114
|
></span>`,
|
|
115
115
|
copyright:
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import FeatureCard from "./FeatureCard.vue";
|
|
2
|
-
|
|
3
|
-
describe("<FeatureCard />", () => {
|
|
4
|
-
it("renders title and content using props", () => {
|
|
5
|
-
const title = "Test Feature";
|
|
6
|
-
const content = "This is a test feature content.";
|
|
7
|
-
|
|
8
|
-
cy.mount(FeatureCard, {
|
|
9
|
-
props: {
|
|
10
|
-
title,
|
|
11
|
-
content,
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
cy.get("h5").should("contain", title);
|
|
16
|
-
// When using content prop without slots, it renders a div with v-html
|
|
17
|
-
cy.contains(content).should("exist");
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("renders content in a paragraph when using slots", () => {
|
|
21
|
-
const title = "Slot Feature";
|
|
22
|
-
const slotContent = "This is content from a slot.";
|
|
23
|
-
|
|
24
|
-
cy.mount(FeatureCard, {
|
|
25
|
-
props: {
|
|
26
|
-
title,
|
|
27
|
-
},
|
|
28
|
-
slots: {
|
|
29
|
-
default: slotContent,
|
|
30
|
-
},
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
cy.get("h5").should("contain", title);
|
|
34
|
-
// When using slots, it renders a p tag
|
|
35
|
-
cy.get("p").should("contain", slotContent);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("renders link when provided", () => {
|
|
39
|
-
const link = {
|
|
40
|
-
text: "Learn More",
|
|
41
|
-
href: "https://example.com",
|
|
42
|
-
target: "_blank",
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
cy.mount(FeatureCard, {
|
|
46
|
-
props: {
|
|
47
|
-
title: "With Link",
|
|
48
|
-
content: "Content",
|
|
49
|
-
link,
|
|
50
|
-
},
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
cy.get("a").should("have.attr", "href", "https://example.com");
|
|
54
|
-
cy.get("a").should("contain", "Learn More");
|
|
55
|
-
cy.get("a").should("have.attr", "target", "_blank");
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("renders icon from class string", () => {
|
|
59
|
-
cy.mount(FeatureCard, {
|
|
60
|
-
props: {
|
|
61
|
-
title: "With Icon",
|
|
62
|
-
content: "Content",
|
|
63
|
-
icon: "mdi-home",
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
cy.get("i.mdi.mdi-home").should("exist");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("renders html icon", () => {
|
|
71
|
-
const iconHtml = '<svg><circle cx="50" cy="50" r="40" /></svg>';
|
|
72
|
-
cy.mount(FeatureCard, {
|
|
73
|
-
props: {
|
|
74
|
-
title: "With SVG Icon",
|
|
75
|
-
content: "Content",
|
|
76
|
-
icon: { html: iconHtml },
|
|
77
|
-
},
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
cy.get(".icon").should("exist");
|
|
81
|
-
cy.get(".icon").find("svg").should("exist");
|
|
82
|
-
});
|
|
83
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
<script setup>
|
|
2
|
-
const { title, content, link, icon } = defineProps({
|
|
3
|
-
title: String,
|
|
4
|
-
content: String,
|
|
5
|
-
link: {
|
|
6
|
-
/** @type {import('vue').PropType<{ text:string ,href: string,target?: string }>} */
|
|
7
|
-
type: Object,
|
|
8
|
-
required: false,
|
|
9
|
-
},
|
|
10
|
-
icon: {
|
|
11
|
-
/** @type {import('vue').PropType<{ html: string, width?: number, height?: number }>} */
|
|
12
|
-
type: Object,
|
|
13
|
-
required: false,
|
|
14
|
-
},
|
|
15
|
-
});
|
|
16
|
-
const iconStyle = {
|
|
17
|
-
width: (icon?.width ?? 40) + "px",
|
|
18
|
-
height: (icon?.height ?? 40) + "px",
|
|
19
|
-
};
|
|
20
|
-
</script>
|
|
21
|
-
<template>
|
|
22
|
-
<article class="vertical large-padding">
|
|
23
|
-
<div>
|
|
24
|
-
<i
|
|
25
|
-
v-if="typeof icon === 'string' && icon.startsWith('mdi-')"
|
|
26
|
-
:class="`mdi ${icon}`"
|
|
27
|
-
></i>
|
|
28
|
-
<div
|
|
29
|
-
v-else-if="icon"
|
|
30
|
-
:style="iconStyle"
|
|
31
|
-
v-html="icon.html"
|
|
32
|
-
class="icon"
|
|
33
|
-
></div>
|
|
34
|
-
<h5 class="small">{{ title }}</h5>
|
|
35
|
-
<p v-if="$slots.default">
|
|
36
|
-
<slot>{{ content }}</slot>
|
|
37
|
-
</p>
|
|
38
|
-
<div v-else v-html="content"></div>
|
|
39
|
-
</div>
|
|
40
|
-
<nav v-if="link">
|
|
41
|
-
<a
|
|
42
|
-
:href="link.href"
|
|
43
|
-
:target="link.target ?? '_blank'"
|
|
44
|
-
class="button transparent bold primary-text no-padding"
|
|
45
|
-
>
|
|
46
|
-
<span>
|
|
47
|
-
{{ link.text }}
|
|
48
|
-
</span>
|
|
49
|
-
<i class="mdi mdi-chevron-right arrow"></i>
|
|
50
|
-
</a>
|
|
51
|
-
</nav>
|
|
52
|
-
</article>
|
|
53
|
-
</template>
|
|
54
|
-
<style scoped>
|
|
55
|
-
/**
|
|
56
|
-
* Undo Vitepress missing with @eox/ui styles
|
|
57
|
-
*/
|
|
58
|
-
.VPHome .vp-doc a.button.bold {
|
|
59
|
-
font-weight: bold;
|
|
60
|
-
}
|
|
61
|
-
.VPHome .vp-doc h5.small {
|
|
62
|
-
font-size: 1.25rem;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Custom styles
|
|
67
|
-
*/
|
|
68
|
-
article {
|
|
69
|
-
justify-content: space-between;
|
|
70
|
-
transition: all 0.3s ease-in-out;
|
|
71
|
-
width: 100%;
|
|
72
|
-
}
|
|
73
|
-
.arrow {
|
|
74
|
-
transition: transform 0.2s;
|
|
75
|
-
}
|
|
76
|
-
.button:hover .arrow {
|
|
77
|
-
transform: translateX(4px);
|
|
78
|
-
}
|
|
79
|
-
.button.transparent:hover:after {
|
|
80
|
-
background: none;
|
|
81
|
-
}
|
|
82
|
-
</style>
|