@sp-days-framework/slidev-theme-sykehuspartner 1.0.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/LICENSE +21 -0
- package/README.md +622 -0
- package/layouts/about-me.vue +410 -0
- package/layouts/center.vue +38 -0
- package/layouts/cover.vue +88 -0
- package/layouts/default.vue +96 -0
- package/layouts/end.vue +54 -0
- package/layouts/fact.vue +39 -0
- package/layouts/full.vue +34 -0
- package/layouts/image-left.vue +222 -0
- package/layouts/image-right.vue +218 -0
- package/layouts/image.vue +143 -0
- package/layouts/intro.vue +315 -0
- package/layouts/quote.vue +72 -0
- package/layouts/section.vue +140 -0
- package/layouts/statement.vue +60 -0
- package/layouts/three-cols-header.vue +103 -0
- package/layouts/three-cols.vue +77 -0
- package/layouts/two-cols-header.vue +95 -0
- package/layouts/two-cols.vue +69 -0
- package/package.json +59 -0
- package/public/sp-banner-dark.svg +37 -0
- package/public/sp-banner-light.svg +37 -0
- package/public/sp-logo-dark.svg +24 -0
- package/public/sp-logo-light.svg +24 -0
- package/setup/index.ts +38 -0
- package/setup/shiki.ts +56 -0
- package/styles/code.css +30 -0
- package/styles/index.ts +4 -0
- package/styles/layout.css +161 -0
- package/uno.config.ts +47 -0
- package/utils/headerContentSplitter.ts +48 -0
- package/utils/layoutHelper.ts +172 -0
package/layouts/fact.vue
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout fact">
|
|
3
|
+
<div v-if="showLogo" class="logo-container">
|
|
4
|
+
<div class="logo-image logo"></div>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="my-auto">
|
|
7
|
+
<slot />
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script setup>
|
|
13
|
+
import { computed } from 'vue'
|
|
14
|
+
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
logo: {
|
|
17
|
+
type: Boolean,
|
|
18
|
+
default: true
|
|
19
|
+
}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const showLogo = computed(() => {
|
|
23
|
+
if ($slidev?.configs?.frontmatter?.logo !== undefined) {
|
|
24
|
+
return $slidev.configs.frontmatter.logo
|
|
25
|
+
}
|
|
26
|
+
return props.logo
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<style scoped>
|
|
31
|
+
.fact {
|
|
32
|
+
height: 100%;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
justify-content: center;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Logo styling moved to global layout.css */
|
|
39
|
+
</style>
|
package/layouts/full.vue
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout full">
|
|
3
|
+
<div v-if="showLogo" class="logo-container">
|
|
4
|
+
<div class="logo-image logo"></div>
|
|
5
|
+
</div>
|
|
6
|
+
<slot />
|
|
7
|
+
</div>
|
|
8
|
+
</template>
|
|
9
|
+
|
|
10
|
+
<script setup>
|
|
11
|
+
import { computed } from 'vue'
|
|
12
|
+
|
|
13
|
+
const props = defineProps({
|
|
14
|
+
logo: {
|
|
15
|
+
type: Boolean,
|
|
16
|
+
default: false
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const showLogo = computed(() => {
|
|
21
|
+
if ($slidev?.configs?.frontmatter?.logo !== undefined) {
|
|
22
|
+
return $slidev.configs.frontmatter.logo
|
|
23
|
+
}
|
|
24
|
+
return props.logo
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<style scoped>
|
|
29
|
+
.full {
|
|
30
|
+
padding: 0;
|
|
31
|
+
height: 100%;
|
|
32
|
+
width: 100%;
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout image-left">
|
|
3
|
+
<div v-if="showLogo" class="logo-container">
|
|
4
|
+
<div class="logo-image logo"></div>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="split-layout">
|
|
7
|
+
<!-- Image container on the left -->
|
|
8
|
+
<div class="image-container" :style="{ width: imageRatioStyle }">
|
|
9
|
+
<div class="image-wrapper" :style="imageContainerStyle">
|
|
10
|
+
<img
|
|
11
|
+
v-if="props.imageSrc"
|
|
12
|
+
:src="getImageUrl(props.imageSrc)"
|
|
13
|
+
:style="combinedImageStyle"
|
|
14
|
+
class="side-image"
|
|
15
|
+
/>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<!-- Content container on the right -->
|
|
20
|
+
<div class="content-container" :style="{ width: contentRatioStyle }">
|
|
21
|
+
<div class="content-layout">
|
|
22
|
+
<div ref="headerRef" class="header"></div>
|
|
23
|
+
<div ref="contentRef" class="content-wrapper">
|
|
24
|
+
<slot />
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
|
|
34
|
+
import { getImageUrl, getImageContainerStyle } from "../utils/layoutHelper";
|
|
35
|
+
import { useHeaderContentSplit } from "../utils/headerContentSplitter";
|
|
36
|
+
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
imageSrc: String,
|
|
39
|
+
imageScale: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: "100%",
|
|
42
|
+
},
|
|
43
|
+
imageAlign: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: "center",
|
|
46
|
+
},
|
|
47
|
+
imageRatio: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: "50%",
|
|
50
|
+
},
|
|
51
|
+
logo: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: true,
|
|
54
|
+
},
|
|
55
|
+
textAlignment: {
|
|
56
|
+
type: String,
|
|
57
|
+
default: "center",
|
|
58
|
+
validator: (value) => ["top", "center", "bottom"].includes(value),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Calculate the image ratio style
|
|
63
|
+
const imageRatioStyle = computed(() => {
|
|
64
|
+
// Ensure it's a percentage string
|
|
65
|
+
const ratio = props.imageRatio.endsWith("%")
|
|
66
|
+
? props.imageRatio
|
|
67
|
+
: `${parseFloat(props.imageRatio)}%`;
|
|
68
|
+
return ratio;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Calculate the content ratio style (remaining space)
|
|
72
|
+
const contentRatioStyle = computed(() => {
|
|
73
|
+
const imageRatio = parseFloat(imageRatioStyle.value);
|
|
74
|
+
return `${100 - imageRatio}%`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Get container alignment style based on imageAlign
|
|
78
|
+
const imageContainerStyle = computed(() => {
|
|
79
|
+
return getImageContainerStyle(props.imageAlign);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Calculate the combined image style including scale
|
|
83
|
+
const combinedImageStyle = computed(() => {
|
|
84
|
+
const style = {
|
|
85
|
+
height: "100%", // Always fill height
|
|
86
|
+
width: "auto",
|
|
87
|
+
maxWidth: "none", // Override default max-width
|
|
88
|
+
objectFit: "contain", // Use 'contain' to prevent cropping by default
|
|
89
|
+
objectPosition: getObjectPosition(props.imageAlign),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Apply scaling if specified
|
|
93
|
+
if (props.imageScale) {
|
|
94
|
+
if (props.imageScale.endsWith("%")) {
|
|
95
|
+
const scale = parseFloat(props.imageScale) / 100;
|
|
96
|
+
style.height = `${scale * 100}%`;
|
|
97
|
+
} else if (!isNaN(Number(props.imageScale))) {
|
|
98
|
+
const scale = Number(props.imageScale);
|
|
99
|
+
style.height = `${scale * 100}%`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return style;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Helper function to convert alignment to object-position value
|
|
107
|
+
function getObjectPosition(align) {
|
|
108
|
+
switch (align /* Lines 93-102 omitted */) {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const showLogo = computed(() => {
|
|
113
|
+
if ($slidev?.configs?.frontmatter?.logo !== undefined) {
|
|
114
|
+
return $slidev.configs.frontmatter.logo;
|
|
115
|
+
}
|
|
116
|
+
return props.logo;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Compute content alignment style based on textAlignment prop
|
|
120
|
+
const contentAlignmentStyle = computed(() => {
|
|
121
|
+
switch (props.textAlignment) {
|
|
122
|
+
case "top":
|
|
123
|
+
return "flex-start";
|
|
124
|
+
case "bottom":
|
|
125
|
+
return "flex-end";
|
|
126
|
+
case "center":
|
|
127
|
+
default:
|
|
128
|
+
return "center";
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Setup for header content split
|
|
133
|
+
const headerRef = ref(null);
|
|
134
|
+
const contentRef = ref(null);
|
|
135
|
+
|
|
136
|
+
// Use our utility to handle the header/content split
|
|
137
|
+
const { setupHeaderSplit } = useHeaderContentSplit(headerRef, contentRef);
|
|
138
|
+
let headerSplitter;
|
|
139
|
+
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
// Setup the header splitter when component is mounted
|
|
142
|
+
headerSplitter = setupHeaderSplit();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
onBeforeUnmount(() => {
|
|
146
|
+
// Clean up when component is unmounted
|
|
147
|
+
if (headerSplitter) {
|
|
148
|
+
headerSplitter.destroy();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<style scoped>
|
|
154
|
+
.image-left {
|
|
155
|
+
height: 100%;
|
|
156
|
+
width: 100%;
|
|
157
|
+
padding: 0;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.split-layout {
|
|
162
|
+
display: flex;
|
|
163
|
+
height: 100%;
|
|
164
|
+
width: 100%;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.image-container {
|
|
168
|
+
height: 100%;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
position: relative;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.image-wrapper {
|
|
174
|
+
height: 100%;
|
|
175
|
+
width: 100%;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.side-image {
|
|
180
|
+
display: block;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.content-container {
|
|
184
|
+
height: 100%;
|
|
185
|
+
overflow: auto;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.content-layout {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
height: 100%;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.content-wrapper {
|
|
195
|
+
padding: 1.5rem 2rem;
|
|
196
|
+
flex: 1;
|
|
197
|
+
overflow-y: auto;
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
justify-content: v-bind(contentAlignmentStyle);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.logo-container {
|
|
204
|
+
position: absolute;
|
|
205
|
+
top: 1rem;
|
|
206
|
+
right: 1rem;
|
|
207
|
+
z-index: 10;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
.header {
|
|
211
|
+
padding: 0 2rem;
|
|
212
|
+
padding-top: 1.5rem;
|
|
213
|
+
margin-bottom: 0;
|
|
214
|
+
width: 100%;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.header:empty {
|
|
218
|
+
display: none;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Logo styling moved to global layout.css */
|
|
222
|
+
</style>
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout image-right">
|
|
3
|
+
<div v-if="showLogo" class="logo-container custom-position">
|
|
4
|
+
<div class="logo-image logo"></div>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="split-layout">
|
|
7
|
+
<!-- Content container on the left -->
|
|
8
|
+
<div class="content-container" :style="{ width: contentRatioStyle }">
|
|
9
|
+
<div class="content-layout">
|
|
10
|
+
<div ref="headerRef" class="header"></div>
|
|
11
|
+
<div ref="contentRef" class="content-wrapper">
|
|
12
|
+
<slot />
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<!-- Image container on the right -->
|
|
18
|
+
<div class="image-container" :style="{ width: imageRatioStyle }">
|
|
19
|
+
<div class="image-wrapper" :style="imageContainerStyle">
|
|
20
|
+
<img
|
|
21
|
+
v-if="props.imageSrc"
|
|
22
|
+
:src="getImageUrl(props.imageSrc)"
|
|
23
|
+
:style="combinedImageStyle"
|
|
24
|
+
class="side-image"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
import { computed, ref, onMounted, onBeforeUnmount } from "vue";
|
|
34
|
+
import { getImageUrl, getImageContainerStyle } from "../utils/layoutHelper";
|
|
35
|
+
import { useHeaderContentSplit } from "../utils/headerContentSplitter";
|
|
36
|
+
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
imageSrc: String,
|
|
39
|
+
imageScale: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: "100%",
|
|
42
|
+
},
|
|
43
|
+
imageAlign: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: "center",
|
|
46
|
+
},
|
|
47
|
+
imageRatio: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: "50%",
|
|
50
|
+
},
|
|
51
|
+
logo: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false,
|
|
54
|
+
},
|
|
55
|
+
textAlignment: {
|
|
56
|
+
type: String,
|
|
57
|
+
default: "center",
|
|
58
|
+
validator: (value) => ["top", "center", "bottom"].includes(value),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Calculate the image ratio style
|
|
63
|
+
const imageRatioStyle = computed(() => {
|
|
64
|
+
// Ensure it's a percentage string
|
|
65
|
+
const ratio = props.imageRatio.endsWith("%")
|
|
66
|
+
? props.imageRatio
|
|
67
|
+
: `${parseFloat(props.imageRatio)}%`;
|
|
68
|
+
return ratio;
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Calculate the content ratio style (remaining space)
|
|
72
|
+
const contentRatioStyle = computed(() => {
|
|
73
|
+
const imageRatio = parseFloat(imageRatioStyle.value);
|
|
74
|
+
return `${100 - imageRatio}%`;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Get container alignment style based on imageAlign
|
|
78
|
+
const imageContainerStyle = computed(() => {
|
|
79
|
+
return getImageContainerStyle(props.imageAlign);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Calculate the combined image style including scale
|
|
83
|
+
const combinedImageStyle = computed(() => {
|
|
84
|
+
const style = {
|
|
85
|
+
height: "100%", // Always fill height
|
|
86
|
+
width: "auto",
|
|
87
|
+
maxWidth: "none", // Override default max-width
|
|
88
|
+
objectFit: "contain", // Use 'contain' to prevent cropping by default
|
|
89
|
+
objectPosition: getObjectPosition(props.imageAlign),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Apply scaling if specified
|
|
93
|
+
if (props.imageScale) {
|
|
94
|
+
if (props.imageScale.endsWith("%")) {
|
|
95
|
+
const scale = parseFloat(props.imageScale) / 100;
|
|
96
|
+
style.height = `${scale * 100}%`;
|
|
97
|
+
} else if (!isNaN(Number(props.imageScale))) {
|
|
98
|
+
const scale = Number(props.imageScale);
|
|
99
|
+
style.height = `${scale * 100}%`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return style;
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Helper function to convert alignment to object-position value
|
|
107
|
+
function getObjectPosition(align) {
|
|
108
|
+
switch (align /* Lines 93-102 omitted */) {
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const showLogo = computed(() => {
|
|
113
|
+
if ($slidev?.configs?.frontmatter?.logo !== undefined) {
|
|
114
|
+
return $slidev.configs.frontmatter.logo;
|
|
115
|
+
}
|
|
116
|
+
return props.logo;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Compute content alignment style based on textAlignment prop
|
|
120
|
+
const contentAlignmentStyle = computed(() => {
|
|
121
|
+
switch (props.textAlignment) {
|
|
122
|
+
case "top":
|
|
123
|
+
return "flex-start";
|
|
124
|
+
case "bottom":
|
|
125
|
+
return "flex-end";
|
|
126
|
+
case "center":
|
|
127
|
+
default:
|
|
128
|
+
return "center";
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Setup for header content split
|
|
133
|
+
const headerRef = ref(null);
|
|
134
|
+
const contentRef = ref(null);
|
|
135
|
+
|
|
136
|
+
// Use our utility to handle the header/content split
|
|
137
|
+
const { setupHeaderSplit } = useHeaderContentSplit(headerRef, contentRef);
|
|
138
|
+
let headerSplitter;
|
|
139
|
+
|
|
140
|
+
onMounted(() => {
|
|
141
|
+
// Setup the header splitter when component is mounted
|
|
142
|
+
headerSplitter = setupHeaderSplit();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
onBeforeUnmount(() => {
|
|
146
|
+
// Clean up when component is unmounted
|
|
147
|
+
if (headerSplitter) {
|
|
148
|
+
headerSplitter.destroy();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<style scoped>
|
|
154
|
+
.image-right {
|
|
155
|
+
height: 100%;
|
|
156
|
+
width: 100%;
|
|
157
|
+
padding: 0;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.split-layout {
|
|
162
|
+
display: flex;
|
|
163
|
+
height: 100%;
|
|
164
|
+
width: 100%;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.image-container {
|
|
168
|
+
height: 100%;
|
|
169
|
+
overflow: hidden;
|
|
170
|
+
position: relative;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.image-wrapper {
|
|
174
|
+
height: 100%;
|
|
175
|
+
width: 100%;
|
|
176
|
+
overflow: hidden;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.side-image {
|
|
180
|
+
display: block;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.content-container {
|
|
184
|
+
height: 100%;
|
|
185
|
+
overflow: auto;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.content-layout {
|
|
189
|
+
display: flex;
|
|
190
|
+
flex-direction: column;
|
|
191
|
+
height: 100%;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.content-wrapper {
|
|
195
|
+
padding: 1.5rem 2rem;
|
|
196
|
+
flex: 1;
|
|
197
|
+
overflow-y: auto;
|
|
198
|
+
display: flex;
|
|
199
|
+
flex-direction: column;
|
|
200
|
+
justify-content: v-bind(contentAlignmentStyle);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.logo-container.custom-position {
|
|
204
|
+
bottom: 1rem;
|
|
205
|
+
left: 1rem;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.header {
|
|
209
|
+
padding: 0 2rem;
|
|
210
|
+
padding-top: 1.5rem;
|
|
211
|
+
margin-bottom: 0;
|
|
212
|
+
width: 100%;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.header:empty {
|
|
216
|
+
display: none;
|
|
217
|
+
}
|
|
218
|
+
</style>
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout image-layout">
|
|
3
|
+
<div v-if="showLogo" class="logo-container">
|
|
4
|
+
<div class="logo-image logo"></div>
|
|
5
|
+
</div>
|
|
6
|
+
<!-- Background image mode - image covers entire slide -->
|
|
7
|
+
<div v-if="useBackground" class="image-background" :style="bgStyle">
|
|
8
|
+
<div class="content">
|
|
9
|
+
<slot />
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Foreground image mode - text at top, image below -->
|
|
14
|
+
<div v-else class="foreground-mode-container">
|
|
15
|
+
<!-- Content area at the top -->
|
|
16
|
+
<div class="content-area">
|
|
17
|
+
<slot />
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<!-- Image area below the content -->
|
|
21
|
+
<div class="image-container" :style="containerStyle">
|
|
22
|
+
<img
|
|
23
|
+
v-if="props.imageSrc"
|
|
24
|
+
:src="getImageUrl(props.imageSrc)"
|
|
25
|
+
:style="imageStyle"
|
|
26
|
+
class="foreground-image"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
32
|
+
|
|
33
|
+
<script setup>
|
|
34
|
+
import { computed } from 'vue'
|
|
35
|
+
import { getImageStyle, getForegroundImageStyle, getImageContainerStyle, getImageUrl } from '../utils/layoutHelper'
|
|
36
|
+
|
|
37
|
+
const props = defineProps({
|
|
38
|
+
imageSrc: String,
|
|
39
|
+
imageScale: {
|
|
40
|
+
type: String,
|
|
41
|
+
default: '70%' // Default to 70% scale for foreground mode
|
|
42
|
+
},
|
|
43
|
+
imageAlign: {
|
|
44
|
+
type: String,
|
|
45
|
+
default: 'center'
|
|
46
|
+
},
|
|
47
|
+
imageBackgroundMode: {
|
|
48
|
+
type: Boolean,
|
|
49
|
+
default: true
|
|
50
|
+
},
|
|
51
|
+
logo: {
|
|
52
|
+
type: Boolean,
|
|
53
|
+
default: false
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Determine whether to use background mode or foreground image
|
|
58
|
+
const useBackground = computed(() => props.imageBackgroundMode)
|
|
59
|
+
|
|
60
|
+
// Get background style if using background mode
|
|
61
|
+
const bgStyle = computed(() => getImageStyle(props.imageSrc, props.imageScale, props.imageAlign))
|
|
62
|
+
|
|
63
|
+
// Get foreground image style if using foreground image
|
|
64
|
+
const imageStyle = computed(() => getForegroundImageStyle(props.imageScale))
|
|
65
|
+
|
|
66
|
+
// Get container style for alignment of foreground image
|
|
67
|
+
const containerStyle = computed(() => getImageContainerStyle(props.imageAlign))
|
|
68
|
+
|
|
69
|
+
// Other utility computed props
|
|
70
|
+
const flexRow = computed(() => props.position === 'left' ? 'flex-row-reverse' : 'flex-row')
|
|
71
|
+
const textItems = computed(() => props.position === 'left' ? 'items-start' : 'items-end')
|
|
72
|
+
const textAlign = computed(() => props.position === 'left' ? 'text-left' : 'text-right')
|
|
73
|
+
|
|
74
|
+
const showLogo = computed(() => {
|
|
75
|
+
if ($slidev?.configs?.frontmatter?.logo !== undefined) {
|
|
76
|
+
return $slidev.configs.frontmatter.logo
|
|
77
|
+
}
|
|
78
|
+
return props.logo
|
|
79
|
+
})
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style scoped>
|
|
83
|
+
.image-layout {
|
|
84
|
+
height: 100%;
|
|
85
|
+
width: 100%;
|
|
86
|
+
padding: 0;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
position: relative;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Background image mode - full screen with content at bottom */
|
|
92
|
+
.image-background {
|
|
93
|
+
height: 100%;
|
|
94
|
+
width: 100%;
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
justify-content: flex-end;
|
|
98
|
+
overflow: hidden;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/* Foreground image mode - text at top, image below */
|
|
102
|
+
.foreground-mode-container {
|
|
103
|
+
display: flex;
|
|
104
|
+
flex-direction: column;
|
|
105
|
+
height: 100%;
|
|
106
|
+
width: 100%;
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.content-area {
|
|
111
|
+
padding: 1.5rem 2rem;
|
|
112
|
+
z-index: 20;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.image-container {
|
|
116
|
+
flex-grow: 1;
|
|
117
|
+
display: flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
padding: 1rem;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.foreground-image {
|
|
125
|
+
max-height: 100%;
|
|
126
|
+
max-width: 100%;
|
|
127
|
+
object-fit: contain;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.content {
|
|
131
|
+
padding: 1.5rem;
|
|
132
|
+
background-color: rgba(0, 0, 0, 0.164);
|
|
133
|
+
border-radius: 0.5rem;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.dark .content {
|
|
137
|
+
background-color: rgba(255, 255, 255, 0.116);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Using global logo container styling */
|
|
141
|
+
|
|
142
|
+
/* Logo styling moved to global layout.css */
|
|
143
|
+
</style>
|