@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.
@@ -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>
@@ -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>