@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,161 @@
|
|
|
1
|
+
/* Light theme colors */
|
|
2
|
+
:root {
|
|
3
|
+
--sp-primary: #023f84;
|
|
4
|
+
--sp-success: #00a400;
|
|
5
|
+
--sp-info: #54c7ec;
|
|
6
|
+
--sp-warning: #ffba00;
|
|
7
|
+
--sp-danger: #fa383e;
|
|
8
|
+
--sp-background: #f7faff;
|
|
9
|
+
--sp-shadow: #00000028;
|
|
10
|
+
--sp-code-background: #53535309;
|
|
11
|
+
|
|
12
|
+
/* Primary color variations (light) */
|
|
13
|
+
--sp-primary-dark: #023977;
|
|
14
|
+
--sp-primary-darker: #023670;
|
|
15
|
+
--sp-primary-darkest: #012c5c;
|
|
16
|
+
--sp-primary-light: #024591;
|
|
17
|
+
--sp-primary-lighter: #024898;
|
|
18
|
+
--sp-primary-lightest: #0352ac;
|
|
19
|
+
|
|
20
|
+
--sp-title-line: var(--sp-primary);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Dark theme colors */
|
|
24
|
+
.dark {
|
|
25
|
+
--sp-primary: #6cace4;
|
|
26
|
+
--sp-success: #88e36b;
|
|
27
|
+
--sp-info: #5dccf0;
|
|
28
|
+
--sp-warning: #ffc93d;
|
|
29
|
+
--sp-danger: #ff6369;
|
|
30
|
+
--sp-background: #0d1117;
|
|
31
|
+
--sp-shadow: #ffffff28;
|
|
32
|
+
--sp-code-background: #01010213;
|
|
33
|
+
|
|
34
|
+
/* Primary color variations inverted (dark) */
|
|
35
|
+
--sp-primary-light: #5ea4e1;
|
|
36
|
+
--sp-primary-lighter: #509cdf;
|
|
37
|
+
--sp-primary-lightest: #338cda;
|
|
38
|
+
--sp-primary-dark: #88bce9;
|
|
39
|
+
--sp-primary-darker: #97c4ec;
|
|
40
|
+
--sp-primary-darkest: #c1dcf4;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Default styling for all layouts */
|
|
44
|
+
.slidev-layout {
|
|
45
|
+
h1 {
|
|
46
|
+
color: var(--sp-primary-dark);
|
|
47
|
+
font-weight: bolder;
|
|
48
|
+
font-size: 2.75rem;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
h2 {
|
|
52
|
+
/* color: var(--sp-primary-darker); */
|
|
53
|
+
font-weight: bold;
|
|
54
|
+
font-size: 2rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
h3 {
|
|
58
|
+
/* color: var(--sp-primary-darkest); */
|
|
59
|
+
font-weight: bolder;
|
|
60
|
+
font-size: 1.55rem;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
background: var(--sp-background);
|
|
64
|
+
padding: 1.5rem 2rem;
|
|
65
|
+
position: relative;
|
|
66
|
+
|
|
67
|
+
:not(pre) > code {
|
|
68
|
+
background: var(--sp-code-background);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* blockquote {
|
|
72
|
+
display: flex;
|
|
73
|
+
align-items: center;
|
|
74
|
+
background: var(--sp-code-background);
|
|
75
|
+
color: var(--slidev-theme-color);
|
|
76
|
+
border-color: #f141a8;
|
|
77
|
+
border-left-width: 3px;
|
|
78
|
+
font-size: var(--slidev-theme-font-size, 1.1em);
|
|
79
|
+
} */
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.banner-image {
|
|
83
|
+
background-image: url("../public/sp-banner-light.svg");
|
|
84
|
+
background-size: contain;
|
|
85
|
+
background-repeat: no-repeat;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.dark .banner-image {
|
|
89
|
+
background-image: url("../public/sp-banner-dark.svg");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.logo-image {
|
|
93
|
+
background-image: url("../public/sp-logo-light.svg");
|
|
94
|
+
background-size: contain;
|
|
95
|
+
background-repeat: no-repeat;
|
|
96
|
+
background-position: center;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.dark .logo-image {
|
|
100
|
+
background-image: url("../public/sp-logo-dark.svg");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Global logo container and styling */
|
|
104
|
+
.logo-container {
|
|
105
|
+
position: absolute;
|
|
106
|
+
z-index: 100; /* Using highest z-index from all layouts */
|
|
107
|
+
width: 2.5rem;
|
|
108
|
+
height: 2.5rem;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* Default positioning for logo (will be overridden by specific layouts if needed) */
|
|
112
|
+
.slidev-layout .logo-container:not([class*="custom-position"]) {
|
|
113
|
+
top: 1rem;
|
|
114
|
+
right: 1rem;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Global logo styling */
|
|
118
|
+
.logo {
|
|
119
|
+
height: 100%;
|
|
120
|
+
width: 100%;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Multi-column layouts */
|
|
124
|
+
.slidev-layout.two-cols,
|
|
125
|
+
.slidev-layout.two-cols-header,
|
|
126
|
+
.slidev-layout.three-cols,
|
|
127
|
+
.slidev-layout.three-cols-header {
|
|
128
|
+
column-gap: 2rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.slidev-layout.three-cols .col-left,
|
|
132
|
+
.slidev-layout.three-cols .col-middle,
|
|
133
|
+
.slidev-layout.three-cols .col-right,
|
|
134
|
+
.slidev-layout.three-cols-header .col-left,
|
|
135
|
+
.slidev-layout.three-cols-header .col-middle,
|
|
136
|
+
.slidev-layout.three-cols-header .col-right {
|
|
137
|
+
flex: 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* Shared styles for cover and intro layouts */
|
|
141
|
+
/* .slidev-layout.cover,
|
|
142
|
+
.slidev-layout.intro {
|
|
143
|
+
height: 100%;
|
|
144
|
+
|
|
145
|
+
h1 {
|
|
146
|
+
font-size: 3.75rem;
|
|
147
|
+
line-height: 5rem;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
h1 + p {
|
|
151
|
+
margin-top: -0.5rem;
|
|
152
|
+
opacity: 0.5;
|
|
153
|
+
margin-bottom: 1rem;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
p + h2,
|
|
157
|
+
ul + h2,
|
|
158
|
+
table + h2 {
|
|
159
|
+
margin-top: 2.5rem;
|
|
160
|
+
}
|
|
161
|
+
} */
|
package/uno.config.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { defineConfig } from 'unocss'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import { globSync } from 'glob'
|
|
4
|
+
|
|
5
|
+
// Function to scan files for icon patterns
|
|
6
|
+
function scanFilesForIcons() {
|
|
7
|
+
try {
|
|
8
|
+
// Find all markdown files in the project directory and all subdirectories
|
|
9
|
+
const slideFiles = globSync('**/*.md', { absolute: true })
|
|
10
|
+
|
|
11
|
+
// Pattern to match icon references like ~i-vscode-icons:file-type-ansible~
|
|
12
|
+
const iconPattern = /~(i-[a-zA-Z0-9-]+:[a-zA-Z0-9-]+)~/g
|
|
13
|
+
|
|
14
|
+
// Set to store unique icons
|
|
15
|
+
const icons = new Set(['i-vscode-icons:file-type-docker']) // Include default icon
|
|
16
|
+
|
|
17
|
+
// Scan each file for icon patterns
|
|
18
|
+
slideFiles.forEach((file: string) => {
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(file, 'utf-8')
|
|
21
|
+
let match
|
|
22
|
+
while ((match = iconPattern.exec(content)) !== null) {
|
|
23
|
+
if (match[1]) {
|
|
24
|
+
icons.add(match[1])
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.warn(`Error reading file ${file}:`, err)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return Array.from(icons)
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.warn('Error scanning files for icons:', err)
|
|
35
|
+
return ['i-vscode-icons:file-type-docker'] // Fallback to default icon
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
safelist: [
|
|
41
|
+
// Static icons that should always be included
|
|
42
|
+
'i-vscode-icons:file-type-docker',
|
|
43
|
+
|
|
44
|
+
// Dynamically scan for icons in slide files
|
|
45
|
+
() => scanFilesForIcons()
|
|
46
|
+
],
|
|
47
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ref, Ref, onMounted } from 'vue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility to split slide content into header and main content sections
|
|
5
|
+
* It extracts the first H1 from content and moves it to a separate header element
|
|
6
|
+
*
|
|
7
|
+
* @param headerRef Ref to the header element where H1 will be moved
|
|
8
|
+
* @param contentRef Ref to the content element containing the original content
|
|
9
|
+
* @returns An object with setup function
|
|
10
|
+
*/
|
|
11
|
+
export function useHeaderContentSplit(headerRef: Ref<HTMLElement | null>, contentRef: Ref<HTMLElement | null>) {
|
|
12
|
+
// Function to move H1 to header section
|
|
13
|
+
const moveH1ToHeader = () => {
|
|
14
|
+
if (!headerRef.value || !contentRef.value) return
|
|
15
|
+
|
|
16
|
+
// Find the first H1 in content
|
|
17
|
+
const firstH1 = contentRef.value.querySelector('h1')
|
|
18
|
+
if (firstH1 && headerRef.value.children.length === 0) {
|
|
19
|
+
// Move it to the header
|
|
20
|
+
headerRef.value.appendChild(firstH1)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const setupHeaderSplit = () => {
|
|
25
|
+
// Initial move when component is mounted
|
|
26
|
+
moveH1ToHeader()
|
|
27
|
+
|
|
28
|
+
// Also set up a MutationObserver to handle dynamically loaded content
|
|
29
|
+
const observer = new MutationObserver(() => {
|
|
30
|
+
// If header is empty and there's an H1 in content, move it
|
|
31
|
+
if (headerRef.value && headerRef.value.children.length === 0) {
|
|
32
|
+
moveH1ToHeader()
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
if (contentRef.value) {
|
|
37
|
+
observer.observe(contentRef.value, { childList: true, subtree: true })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
destroy: () => observer.disconnect()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
setupHeaderSplit
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { CSSProperties } from "vue";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple function to resolve image paths
|
|
5
|
+
* Works for URLs, absolute paths, and relative paths
|
|
6
|
+
*/
|
|
7
|
+
export function getImageUrl(path?: string): string {
|
|
8
|
+
if (!path) return "";
|
|
9
|
+
|
|
10
|
+
// If it's a color, don't process it
|
|
11
|
+
if (path.startsWith("#") || path.startsWith("rgb")) return path;
|
|
12
|
+
|
|
13
|
+
// If it's a full URL, use it as is
|
|
14
|
+
if (path.match(/^https?:\/\//)) return path;
|
|
15
|
+
|
|
16
|
+
// If it's a root-relative path (starts with /), use it as is
|
|
17
|
+
if (path.startsWith("/")) return path;
|
|
18
|
+
|
|
19
|
+
// Otherwise, treat as relative path and add leading slash
|
|
20
|
+
return `/${path}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Generate background style object for image or color
|
|
25
|
+
* @param imagePath - Path to the image or color string
|
|
26
|
+
* @param scale - Optional scale factor for the image (e.g., '50%', '0.5')
|
|
27
|
+
* @param align - Optional alignment for the image (e.g., 'top', 'bottom', 'left', 'right')
|
|
28
|
+
*/
|
|
29
|
+
export function getImageStyle(
|
|
30
|
+
imagePath?: string,
|
|
31
|
+
scale?: string,
|
|
32
|
+
align?: string
|
|
33
|
+
): CSSProperties {
|
|
34
|
+
if (!imagePath) return {};
|
|
35
|
+
|
|
36
|
+
// If it's a color, set as background color
|
|
37
|
+
if (imagePath.startsWith("#") || imagePath.startsWith("rgb")) {
|
|
38
|
+
return {
|
|
39
|
+
backgroundColor: imagePath,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Parse alignment
|
|
44
|
+
let backgroundPosition = "center";
|
|
45
|
+
if (align) {
|
|
46
|
+
// Handle single-direction alignments
|
|
47
|
+
if (["top", "bottom", "left", "right"].includes(align)) {
|
|
48
|
+
backgroundPosition = align;
|
|
49
|
+
}
|
|
50
|
+
// Handle combined alignments (e.g., 'top left', 'bottom right')
|
|
51
|
+
else if (align.includes(" ")) {
|
|
52
|
+
backgroundPosition = align;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Parse scale and convert to background-size
|
|
57
|
+
let backgroundSize = "contain"; // Default to 'contain' to prevent cropping
|
|
58
|
+
if (scale) {
|
|
59
|
+
// If scale is a percentage string
|
|
60
|
+
if (scale.endsWith("%")) {
|
|
61
|
+
// For 100%, use 'contain' to show the full image without cropping
|
|
62
|
+
if (scale === "100%") {
|
|
63
|
+
backgroundSize = "contain";
|
|
64
|
+
} else {
|
|
65
|
+
backgroundSize = scale;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// If scale is a decimal number
|
|
69
|
+
else if (!isNaN(Number(scale))) {
|
|
70
|
+
const scaleNum = Number(scale);
|
|
71
|
+
// For scale = 1.0 (100%), use 'contain' to show the full image
|
|
72
|
+
if (scaleNum === 1.0) {
|
|
73
|
+
backgroundSize = "contain";
|
|
74
|
+
} else {
|
|
75
|
+
backgroundSize = `${scaleNum * 100}%`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// If scale is 'contain' or 'cover'
|
|
79
|
+
else if (["contain", "cover", "auto"].includes(scale)) {
|
|
80
|
+
backgroundSize = scale;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Return combined style
|
|
85
|
+
return {
|
|
86
|
+
backgroundImage: `url("${getImageUrl(imagePath)}")`,
|
|
87
|
+
backgroundPosition,
|
|
88
|
+
backgroundSize,
|
|
89
|
+
backgroundRepeat: "no-repeat",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate style for a foreground image element (not background)
|
|
95
|
+
* @param scale - Optional scale factor for the image (e.g., '50%', '0.5')
|
|
96
|
+
* @param align - Optional alignment for the image container (e.g., 'center', 'flex-start', 'flex-end')
|
|
97
|
+
*/
|
|
98
|
+
export function getForegroundImageStyle(
|
|
99
|
+
scale?: string,
|
|
100
|
+
align?: string
|
|
101
|
+
): CSSProperties {
|
|
102
|
+
const style: CSSProperties = {
|
|
103
|
+
maxWidth: "100%",
|
|
104
|
+
maxHeight: "100%",
|
|
105
|
+
objectFit: "contain",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// Apply scaling if specified
|
|
109
|
+
if (scale) {
|
|
110
|
+
// If scale is a percentage string
|
|
111
|
+
if (scale.endsWith("%")) {
|
|
112
|
+
style.width = scale;
|
|
113
|
+
}
|
|
114
|
+
// If scale is a decimal number
|
|
115
|
+
else if (!isNaN(Number(scale))) {
|
|
116
|
+
const scaleNum = Number(scale);
|
|
117
|
+
style.width = `${scaleNum * 100}%`;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return style;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generate container style for image alignment
|
|
126
|
+
* @param align - Alignment direction ('center', 'top', 'bottom', 'left', 'right', etc)
|
|
127
|
+
*/
|
|
128
|
+
export function getImageContainerStyle(align?: string): CSSProperties {
|
|
129
|
+
const style: CSSProperties = {
|
|
130
|
+
display: "flex",
|
|
131
|
+
justifyContent: "center",
|
|
132
|
+
alignItems: "center",
|
|
133
|
+
height: "100%",
|
|
134
|
+
width: "100%",
|
|
135
|
+
overflow: "hidden",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (align) {
|
|
139
|
+
switch (align) {
|
|
140
|
+
case "top":
|
|
141
|
+
style.alignItems = "flex-start";
|
|
142
|
+
break;
|
|
143
|
+
case "bottom":
|
|
144
|
+
style.alignItems = "flex-end";
|
|
145
|
+
break;
|
|
146
|
+
case "left":
|
|
147
|
+
style.justifyContent = "flex-start";
|
|
148
|
+
break;
|
|
149
|
+
case "right":
|
|
150
|
+
style.justifyContent = "flex-end";
|
|
151
|
+
break;
|
|
152
|
+
case "top-left":
|
|
153
|
+
style.alignItems = "flex-start";
|
|
154
|
+
style.justifyContent = "flex-start";
|
|
155
|
+
break;
|
|
156
|
+
case "top-right":
|
|
157
|
+
style.alignItems = "flex-start";
|
|
158
|
+
style.justifyContent = "flex-end";
|
|
159
|
+
break;
|
|
160
|
+
case "bottom-left":
|
|
161
|
+
style.alignItems = "flex-end";
|
|
162
|
+
style.justifyContent = "flex-start";
|
|
163
|
+
break;
|
|
164
|
+
case "bottom-right":
|
|
165
|
+
style.alignItems = "flex-end";
|
|
166
|
+
style.justifyContent = "flex-end";
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return style;
|
|
172
|
+
}
|