@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
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout about-me">
|
|
3
|
+
<!-- When no image, show content aligned to the left -->
|
|
4
|
+
<div v-if="!props.imageSrc" class="content-no-image">
|
|
5
|
+
<div class="text-content text-left">
|
|
6
|
+
<!-- Banner logo -->
|
|
7
|
+
<div v-if="props.logo" class="banner-container">
|
|
8
|
+
<div class="banner-image banner"></div>
|
|
9
|
+
</div>
|
|
10
|
+
<h1 v-if="props.name" class="name">{{ props.name }}</h1>
|
|
11
|
+
<div v-if="props.jobTitle || props.department" class="job-info">
|
|
12
|
+
<p v-if="props.jobTitle" class="job-title">{{ props.jobTitle }}</p>
|
|
13
|
+
<p v-if="props.department" class="department">{{ props.department }}</p>
|
|
14
|
+
</div>
|
|
15
|
+
<p v-if="props.description" class="description">{{ props.description }}</p>
|
|
16
|
+
<div v-if="props.email || props.linkedin" class="contact-info">
|
|
17
|
+
<div v-if="props.email" class="contact-item">
|
|
18
|
+
<!-- SVG Email icon that changes with dark/light mode -->
|
|
19
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="contact-icon email-icon">
|
|
20
|
+
<path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2zm-2 0l-8 5l-8-5zm0 12H4V8l8 5l8-5z"/>
|
|
21
|
+
</svg>
|
|
22
|
+
<span class="email">{{ props.email }}</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div v-if="props.linkedin" class="contact-item">
|
|
25
|
+
<!-- SVG LinkedIn icon that changes with dark/light mode -->
|
|
26
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="contact-icon linkedin-icon">
|
|
27
|
+
<path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93zM6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37z"/>
|
|
28
|
+
</svg>
|
|
29
|
+
<span class="linkedin">{{ props.linkedin }}</span>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Split layout with image -->
|
|
36
|
+
<div v-else class="split-layout" :class="layoutClass">
|
|
37
|
+
<!-- Balanced split layout with equal halves -->
|
|
38
|
+
<div class="half-container image-half">
|
|
39
|
+
<div class="image-container" :style="containerStyle">
|
|
40
|
+
<div class="image-wrapper" :class="{ 'circle': props.imageShape === 'circle' }" :style="containerStyle">
|
|
41
|
+
<img
|
|
42
|
+
:src="getImageUrl(props.imageSrc)"
|
|
43
|
+
:style="imageStyle"
|
|
44
|
+
class="profile-image"
|
|
45
|
+
:class="{ 'circle': props.imageShape === 'circle' }"
|
|
46
|
+
alt="Profile image"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="half-container content-half">
|
|
53
|
+
<div class="text-container" :class="textAlignClass">
|
|
54
|
+
<div class="text-content">
|
|
55
|
+
<!-- Banner logo -->
|
|
56
|
+
<div v-if="props.logo" class="banner-container">
|
|
57
|
+
<div class="banner-image banner"></div>
|
|
58
|
+
</div>
|
|
59
|
+
<h1 v-if="props.name" class="name">{{ props.name }}</h1>
|
|
60
|
+
<div v-if="props.jobTitle || props.department" class="job-info">
|
|
61
|
+
<p v-if="props.jobTitle" class="job-title">{{ props.jobTitle }}</p>
|
|
62
|
+
<p v-if="props.department" class="department">{{ props.department }}</p>
|
|
63
|
+
</div>
|
|
64
|
+
<p v-if="props.description" class="description">{{ props.description }}</p>
|
|
65
|
+
<div v-if="props.email || props.linkedin" class="contact-info">
|
|
66
|
+
<div v-if="props.email" class="contact-item">
|
|
67
|
+
<!-- SVG Email icon that changes with dark/light mode -->
|
|
68
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="contact-icon email-icon">
|
|
69
|
+
<path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2zm-2 0l-8 5l-8-5zm0 12H4V8l8 5l8-5z"/>
|
|
70
|
+
</svg>
|
|
71
|
+
<span class="email">{{ props.email }}</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div v-if="props.linkedin" class="contact-item">
|
|
74
|
+
<!-- SVG LinkedIn icon that changes with dark/light mode -->
|
|
75
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="contact-icon linkedin-icon">
|
|
76
|
+
<path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2zm-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93zM6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37z"/>
|
|
77
|
+
</svg>
|
|
78
|
+
<span class="linkedin">{{ props.linkedin }}</span>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</template>
|
|
87
|
+
|
|
88
|
+
<script setup>
|
|
89
|
+
import { computed } from 'vue'
|
|
90
|
+
import { getImageUrl } from '../utils/layoutHelper'
|
|
91
|
+
|
|
92
|
+
const props = defineProps({
|
|
93
|
+
// Image options
|
|
94
|
+
imageSrc: String,
|
|
95
|
+
imageShape: {
|
|
96
|
+
type: String,
|
|
97
|
+
default: 'rectangle',
|
|
98
|
+
validator: (value) => ['rectangle', 'circle'].includes(value)
|
|
99
|
+
},
|
|
100
|
+
imageSize: {
|
|
101
|
+
type: String,
|
|
102
|
+
default: '100%'
|
|
103
|
+
},
|
|
104
|
+
imagePosition: {
|
|
105
|
+
type: String,
|
|
106
|
+
default: 'left',
|
|
107
|
+
validator: (value) => ['left', 'right'].includes(value)
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
// Content options
|
|
111
|
+
name: String,
|
|
112
|
+
jobTitle: String,
|
|
113
|
+
department: String,
|
|
114
|
+
description: String,
|
|
115
|
+
email: String,
|
|
116
|
+
linkedin: String,
|
|
117
|
+
|
|
118
|
+
// Logo options
|
|
119
|
+
logo: {
|
|
120
|
+
type: Boolean,
|
|
121
|
+
default: true
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Compute layout class based on image position
|
|
126
|
+
const layoutClass = computed(() => {
|
|
127
|
+
return props.imagePosition === 'left' ? 'image-left' : 'image-right';
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Compute text alignment based on image position
|
|
131
|
+
const textAlignClass = computed(() => {
|
|
132
|
+
return props.imagePosition === 'left' ? 'text-left' : 'text-right';
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Calculate if we have an image
|
|
136
|
+
const hasImage = computed(() => Boolean(props.imageSrc));
|
|
137
|
+
|
|
138
|
+
// Calculate image style
|
|
139
|
+
const imageStyle = computed(() => {
|
|
140
|
+
const style = {
|
|
141
|
+
maxWidth: '100%',
|
|
142
|
+
maxHeight: '100%',
|
|
143
|
+
objectFit: 'cover'
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
return style;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Calculate container style based on image size
|
|
150
|
+
const containerStyle = computed(() => {
|
|
151
|
+
const style = {};
|
|
152
|
+
|
|
153
|
+
if (props.imageSize) {
|
|
154
|
+
if (props.imageSize.endsWith('%')) {
|
|
155
|
+
style.width = props.imageSize;
|
|
156
|
+
style.height = props.imageSize;
|
|
157
|
+
} else if (!isNaN(Number(props.imageSize))) {
|
|
158
|
+
style.width = `${parseFloat(props.imageSize) * 100}%`;
|
|
159
|
+
style.height = `${parseFloat(props.imageSize) * 100}%`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return style;
|
|
164
|
+
});
|
|
165
|
+
</script>
|
|
166
|
+
|
|
167
|
+
<style scoped>
|
|
168
|
+
.about-me {
|
|
169
|
+
height: 100%;
|
|
170
|
+
width: 100%;
|
|
171
|
+
padding: 0;
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: center;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Split layout styling */
|
|
178
|
+
.split-layout {
|
|
179
|
+
display: flex;
|
|
180
|
+
width: 100%;
|
|
181
|
+
height: 100%;
|
|
182
|
+
align-items: center;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.image-left {
|
|
186
|
+
flex-direction: row;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.image-right {
|
|
190
|
+
flex-direction: row-reverse;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Half container for equal balance */
|
|
194
|
+
.half-container {
|
|
195
|
+
width: 50%;
|
|
196
|
+
height: 100%;
|
|
197
|
+
display: flex;
|
|
198
|
+
align-items: center;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.image-half {
|
|
202
|
+
justify-content: flex-end;
|
|
203
|
+
padding-right: 2rem;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.content-half {
|
|
207
|
+
justify-content: flex-start;
|
|
208
|
+
padding-left: 2rem;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.image-right .image-half {
|
|
212
|
+
/* justify-content: flex-start; */
|
|
213
|
+
padding-right: 0;
|
|
214
|
+
padding-left: 2rem;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.image-right .content-half {
|
|
218
|
+
/* justify-content: flex-start; */
|
|
219
|
+
padding-left: 0;
|
|
220
|
+
padding-right: 2rem;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* Image container */
|
|
224
|
+
.image-container {
|
|
225
|
+
height: 80%;
|
|
226
|
+
width: 80%;
|
|
227
|
+
display: flex;
|
|
228
|
+
align-items: center;
|
|
229
|
+
justify-content: center;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.image-left .image-container {
|
|
233
|
+
justify-content: flex-end; /* Align to the right when image is on left */
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.image-right .image-container {
|
|
237
|
+
justify-content: flex-start; /* Align to the left when image is on right */
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.image-wrapper {
|
|
241
|
+
display: flex;
|
|
242
|
+
align-items: center;
|
|
243
|
+
justify-content: center;
|
|
244
|
+
overflow: hidden;
|
|
245
|
+
border: 1px solid var(--sp-primary-darkest);
|
|
246
|
+
box-shadow: 0 4px 12px var(--sp-shadow);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.image-wrapper.circle {
|
|
250
|
+
border-radius: 50%;
|
|
251
|
+
overflow: hidden;
|
|
252
|
+
max-width: 20em;
|
|
253
|
+
max-height: 20em;
|
|
254
|
+
aspect-ratio: 1 / 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.profile-image {
|
|
258
|
+
object-fit: cover;
|
|
259
|
+
width: 100%;
|
|
260
|
+
height: 100%;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.profile-image.circle {
|
|
264
|
+
width: 100%;
|
|
265
|
+
height: 100%;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* Text container */
|
|
269
|
+
.text-container {
|
|
270
|
+
width: 100%;
|
|
271
|
+
display: flex;
|
|
272
|
+
align-items: center;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.text-left .text-content {
|
|
276
|
+
text-align: left;
|
|
277
|
+
width: 100%;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.text-right .text-content {
|
|
281
|
+
text-align: right;
|
|
282
|
+
width: 100%;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Content when no image - left aligned */
|
|
286
|
+
.content-no-image {
|
|
287
|
+
width: 100%;
|
|
288
|
+
height: 100%;
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
justify-content: flex-start;
|
|
292
|
+
padding-left: 4rem;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.content-no-image .text-content {
|
|
296
|
+
width: 100%;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* Text styling */
|
|
300
|
+
.name {
|
|
301
|
+
color: inherit;
|
|
302
|
+
margin-bottom: 0.5rem;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.name::after {
|
|
306
|
+
content: '';
|
|
307
|
+
display: block;
|
|
308
|
+
width: 50%;
|
|
309
|
+
margin-top: 0.5rem;
|
|
310
|
+
margin-bottom: 1.5rem;
|
|
311
|
+
height: 4px;
|
|
312
|
+
background-color: var(--sp-title-line);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.text-right .name::after {
|
|
316
|
+
margin-left: auto;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.job-info {
|
|
320
|
+
margin-bottom: 1rem;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
.job-title {
|
|
324
|
+
color: var(--sp-primary-lighter);
|
|
325
|
+
margin: 0;
|
|
326
|
+
font-weight: 700;
|
|
327
|
+
font-size: 1.3em;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.department {
|
|
331
|
+
color: var(--sp-primary-lighter);
|
|
332
|
+
margin: 0;
|
|
333
|
+
font-weight: 500;
|
|
334
|
+
font-size: 1em;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.description {
|
|
338
|
+
margin-bottom: 1.5rem;
|
|
339
|
+
font-style: italic;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.contact-info {
|
|
343
|
+
display: flex;
|
|
344
|
+
flex-direction: column;
|
|
345
|
+
gap: 0.5rem;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.contact-item {
|
|
349
|
+
display: flex;
|
|
350
|
+
align-items: center;
|
|
351
|
+
gap: 0.5rem;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
.text-right .contact-item {
|
|
355
|
+
justify-content: flex-end;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.email {
|
|
359
|
+
color: var(--sp-primary-light);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.linkedin {
|
|
363
|
+
color: var(--sp-primary-lighter);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* SVG icons */
|
|
367
|
+
.contact-icon {
|
|
368
|
+
width: 24px;
|
|
369
|
+
height: 24px;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.email-icon path {
|
|
373
|
+
fill: var(--sp-primary-lighter);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.linkedin-icon path {
|
|
377
|
+
fill: var(--sp-primary-lighter);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
.dark .email-icon path {
|
|
381
|
+
fill: var(--sp-primary-darker);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
.dark .linkedin-icon path {
|
|
385
|
+
fill: var(--sp-primary-darker);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/* Banner styles */
|
|
389
|
+
.banner-container {
|
|
390
|
+
width: auto;
|
|
391
|
+
margin-bottom: 1rem;
|
|
392
|
+
max-width: 150px;
|
|
393
|
+
height: 2rem;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.banner {
|
|
397
|
+
width: 100%;
|
|
398
|
+
height: 100%;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.image-left .banner-container {
|
|
402
|
+
margin-left: -0rem; /* Move the banner to the left beyond the border */
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.text-right .banner-container {
|
|
406
|
+
margin-left: auto; /* Push the banner container to the right */
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* Banner styling handled by global CSS */
|
|
410
|
+
</style>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout center">
|
|
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
|
+
.center {
|
|
32
|
+
height: 100%;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout cover">
|
|
3
|
+
<div v-if="showBanner" class="banner-container">
|
|
4
|
+
<div class="banner-image banner"></div>
|
|
5
|
+
</div>
|
|
6
|
+
<div class="my-auto w-full flex flex-col items-center justify-center text-center">
|
|
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 showBanner = 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
|
+
.cover {
|
|
32
|
+
height: 100%;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Using :deep to target elements inside the slot */
|
|
40
|
+
:deep(h1) {
|
|
41
|
+
color: inherit;
|
|
42
|
+
/* Use clamp for responsive sizing between 3rem and 4rem */
|
|
43
|
+
font-size: clamp(3rem, 4rem - 0.5rem * var(--slidev-content-size, 1), 4rem);
|
|
44
|
+
line-height: 1.25; /* Proportional line-height */
|
|
45
|
+
/* Prevent overflow with ellipsis */
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
text-overflow: ellipsis;
|
|
48
|
+
/* Ensure words don't overflow */
|
|
49
|
+
word-wrap: break-word;
|
|
50
|
+
/* Helps with long single words */
|
|
51
|
+
overflow-wrap: break-word;
|
|
52
|
+
/* Maximum of 2 lines before wrapping */
|
|
53
|
+
max-height: calc(2.5 * 4rem);
|
|
54
|
+
display: -webkit-box;
|
|
55
|
+
-webkit-line-clamp: 2; /* Show max 2 lines */
|
|
56
|
+
line-clamp: 2; /* Standard property for compatibility */
|
|
57
|
+
-webkit-box-orient: vertical;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
:deep(h1)::after {
|
|
61
|
+
content: '';
|
|
62
|
+
display: block;
|
|
63
|
+
width: 50%;
|
|
64
|
+
margin: 0.5rem auto 1.5rem;
|
|
65
|
+
height: 4px;
|
|
66
|
+
background-color: var(--sp-title-line);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
:deep(h2) {
|
|
70
|
+
color: var(--sp-primary-darkest);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.banner-container {
|
|
74
|
+
position: absolute;
|
|
75
|
+
bottom: 0%;
|
|
76
|
+
left: 50%;
|
|
77
|
+
transform: translate(-50%, -50%);
|
|
78
|
+
z-index: 10;
|
|
79
|
+
width: 80%;
|
|
80
|
+
max-width: 200px;
|
|
81
|
+
height: 3rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.banner {
|
|
85
|
+
width: 100%;
|
|
86
|
+
height: 100%;
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout default">
|
|
3
|
+
<div v-if="showLogo" class="logo-container">
|
|
4
|
+
<div class="logo-image logo"></div>
|
|
5
|
+
</div>
|
|
6
|
+
<div ref="headerRef" class="header"></div>
|
|
7
|
+
<div ref="contentRef" class="content">
|
|
8
|
+
<slot />
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup>
|
|
14
|
+
import { computed, onMounted, ref, onBeforeUnmount } from 'vue'
|
|
15
|
+
import { useHeaderContentSplit } from '../utils/headerContentSplitter'
|
|
16
|
+
|
|
17
|
+
const props = defineProps({
|
|
18
|
+
logo: {
|
|
19
|
+
type: Boolean,
|
|
20
|
+
default: true
|
|
21
|
+
},
|
|
22
|
+
textAlignment: {
|
|
23
|
+
type: String,
|
|
24
|
+
default: 'center',
|
|
25
|
+
validator: (value) => ['top', 'center', 'bottom'].includes(value)
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const headerRef = ref(null)
|
|
30
|
+
const contentRef = ref(null)
|
|
31
|
+
|
|
32
|
+
const showLogo = computed(() => {
|
|
33
|
+
if ($slidev?.configs?.frontmatter?.logo !== undefined) {
|
|
34
|
+
return $slidev.configs.frontmatter.logo
|
|
35
|
+
}
|
|
36
|
+
return props.logo
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// Compute content alignment style based on textAlignment prop
|
|
40
|
+
const contentAlignmentStyle = computed(() => {
|
|
41
|
+
switch (props.textAlignment) {
|
|
42
|
+
case 'top':
|
|
43
|
+
return 'flex-start'
|
|
44
|
+
case 'bottom':
|
|
45
|
+
return 'flex-end'
|
|
46
|
+
case 'center':
|
|
47
|
+
default:
|
|
48
|
+
return 'center'
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// Use our utility to handle the header/content split
|
|
53
|
+
const { setupHeaderSplit } = useHeaderContentSplit(headerRef, contentRef)
|
|
54
|
+
let headerSplitter
|
|
55
|
+
|
|
56
|
+
onMounted(() => {
|
|
57
|
+
// Setup the header splitter when component is mounted
|
|
58
|
+
headerSplitter = setupHeaderSplit()
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
onBeforeUnmount(() => {
|
|
62
|
+
// Clean up when component is unmounted
|
|
63
|
+
if (headerSplitter) {
|
|
64
|
+
headerSplitter.destroy()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
</script>
|
|
68
|
+
|
|
69
|
+
<style scoped>
|
|
70
|
+
.default {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
height: 100%;
|
|
74
|
+
position: relative;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.header {
|
|
78
|
+
margin-bottom: 2rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.header:empty {
|
|
82
|
+
display: none;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.content {
|
|
86
|
+
flex: 1;
|
|
87
|
+
display: flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
justify-content: v-bind(contentAlignmentStyle);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:deep(h1) {
|
|
93
|
+
margin-top: 0;
|
|
94
|
+
margin-bottom: 0;
|
|
95
|
+
}
|
|
96
|
+
</style>
|
package/layouts/end.vue
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="slidev-layout end">
|
|
3
|
+
<div v-if="showBanner" class="banner-container">
|
|
4
|
+
<div class="banner-image banner"></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 showBanner = 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
|
+
.end {
|
|
32
|
+
height: 100%;
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
align-items: center;
|
|
36
|
+
justify-content: center;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.banner-container {
|
|
40
|
+
position: absolute;
|
|
41
|
+
bottom: 0%;
|
|
42
|
+
left: 50%;
|
|
43
|
+
transform: translate(-50%, -50%);
|
|
44
|
+
z-index: 10;
|
|
45
|
+
width: 60%;
|
|
46
|
+
max-width: 300px;
|
|
47
|
+
height: 3rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.banner {
|
|
51
|
+
width: 100%;
|
|
52
|
+
height: 100%;
|
|
53
|
+
}
|
|
54
|
+
</style>
|