@miragon/slidev-toolkit 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/assets/fonts/Geist-Variable.woff2 +0 -0
- package/assets/fonts/GeistMono-Variable.woff2 +0 -0
- package/assets/fonts/LICENSE.txt +92 -0
- package/assets/komet.svg +1 -0
- package/assets/logo.svg +1 -0
- package/components/Agenda.vue +517 -0
- package/components/BrandMeshBackground.vue +159 -0
- package/components/Card.vue +72 -0
- package/components/CardGrid.vue +46 -0
- package/components/Figure.vue +101 -0
- package/components/SplitView.vue +71 -0
- package/components/Step.vue +19 -0
- package/components/StepList.vue +28 -0
- package/layouts/bpmn.vue +162 -0
- package/layouts/closing.vue +127 -0
- package/layouts/compare.vue +214 -0
- package/layouts/content-image.vue +182 -0
- package/layouts/content.vue +186 -0
- package/layouts/cover.vue +114 -0
- package/layouts/goodbad.vue +308 -0
- package/layouts/hero.vue +168 -0
- package/layouts/person.vue +287 -0
- package/layouts/section.vue +127 -0
- package/layouts/showcase.vue +244 -0
- package/package.json +38 -0
- package/styles/fonts.css +23 -0
- package/styles/index.css +25 -0
- package/styles/theme.css +80 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* person — Person(en) vorstellen (STATISCHE Welt, kein Mesh-Shader).
|
|
4
|
+
*
|
|
5
|
+
* Folgt der `hero`-Bildsprache: heller Grund, viel Weißraum, große Typografie,
|
|
6
|
+
* sparsamer Gradient-Akzent.
|
|
7
|
+
*
|
|
8
|
+
* Zwei Modi:
|
|
9
|
+
* • Einzeln — Porträt + Textblock (Name, Rolle, Bio als Slot).
|
|
10
|
+
* • Duo — sobald `name2` gesetzt ist: zwei Speaker nebeneinander.
|
|
11
|
+
*
|
|
12
|
+
* Porträt-Rahmen ist robust: zeigt `photo`, wenn gesetzt — sonst Initialen aus
|
|
13
|
+
* dem Namen auf einem Gradient-Rahmen (nie ein kaputtes Bild-Icon). Der Rahmen
|
|
14
|
+
* ist im Hochformat gehalten, passend zu freigestellten Brand-Fotos.
|
|
15
|
+
*
|
|
16
|
+
* Frontmatter-Props (keine Multi-Line-Props → 2. Person über *2-Felder):
|
|
17
|
+
* name, role, photo — erste Person
|
|
18
|
+
* name2, role2, photo2 — zweite Person (aktiviert den Duo-Modus)
|
|
19
|
+
* eyebrow — kleine Überzeile (optional)
|
|
20
|
+
* accent — "blue" | "green" | "mixed" — Gradient-Akzent (default blue)
|
|
21
|
+
* side — "left" | "right" — Porträt-Seite im Einzel-Modus (default left)
|
|
22
|
+
* Slot: Kurzbio (einzeln) bzw. optionaler zentrierter Einleitungssatz (Duo).
|
|
23
|
+
*/
|
|
24
|
+
import { computed } from 'vue'
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(
|
|
27
|
+
defineProps<{
|
|
28
|
+
name?: string
|
|
29
|
+
role?: string
|
|
30
|
+
photo?: string
|
|
31
|
+
name2?: string
|
|
32
|
+
role2?: string
|
|
33
|
+
photo2?: string
|
|
34
|
+
eyebrow?: string
|
|
35
|
+
accent?: 'blue' | 'green' | 'mixed'
|
|
36
|
+
side?: 'left' | 'right'
|
|
37
|
+
}>(),
|
|
38
|
+
{ accent: 'blue', side: 'left' },
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const isDuo = computed(() => !!props.name2)
|
|
42
|
+
|
|
43
|
+
// Base-Pfad respektieren (GitHub Pages baut unter /<repo>/). Runtime-Strings
|
|
44
|
+
// werden von Vite NICHT umgeschrieben — daher manuell mit BASE_URL auflösen.
|
|
45
|
+
function withBase(path?: string) {
|
|
46
|
+
if (!path) return path
|
|
47
|
+
if (/^https?:\/\//.test(path)) return path
|
|
48
|
+
return import.meta.env.BASE_URL.replace(/\/$/, '') + '/' + path.replace(/^\//, '')
|
|
49
|
+
}
|
|
50
|
+
const photoSrc = computed(() => withBase(props.photo))
|
|
51
|
+
const photo2Src = computed(() => withBase(props.photo2))
|
|
52
|
+
|
|
53
|
+
// Token reaktiv (siehe hero.vue) — alle Werte aus theme.css, keine Hex.
|
|
54
|
+
const gradientVar = computed(() => `var(--miragon-gradient-${props.accent})`)
|
|
55
|
+
const roleVar = computed(() =>
|
|
56
|
+
props.accent === 'green' ? 'var(--miragon-green-deep)' : 'var(--miragon-blue)',
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
function toInitials(name?: string) {
|
|
60
|
+
const parts = (name ?? '').trim().split(/\s+/).filter(Boolean)
|
|
61
|
+
if (parts.length === 0) return '·'
|
|
62
|
+
const first = parts[0][0] ?? ''
|
|
63
|
+
const last = parts.length > 1 ? parts[parts.length - 1][0] ?? '' : ''
|
|
64
|
+
return (first + last).toUpperCase()
|
|
65
|
+
}
|
|
66
|
+
const initials = computed(() => toInitials(props.name))
|
|
67
|
+
const initials2 = computed(() => toInitials(props.name2))
|
|
68
|
+
|
|
69
|
+
function onPhotoError(e: Event) {
|
|
70
|
+
// Bild fehlt → Rahmen leeren, Initialen-Fallback übernimmt das Layout drumherum.
|
|
71
|
+
;(e.target as HTMLImageElement).style.display = 'none'
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div
|
|
77
|
+
class="person-layout"
|
|
78
|
+
:class="[isDuo ? 'is-duo' : `side-${side}`]"
|
|
79
|
+
:style="{ '--p-grad': gradientVar, '--p-role': roleVar }"
|
|
80
|
+
>
|
|
81
|
+
<!-- ────────── Einzel-Modus ────────── -->
|
|
82
|
+
<div v-if="!isDuo" class="person-content">
|
|
83
|
+
<figure class="person-figure">
|
|
84
|
+
<img v-if="photo" :src="photoSrc" :alt="name" class="person-photo" @error="onPhotoError" />
|
|
85
|
+
<span v-else class="person-initials">{{ initials }}</span>
|
|
86
|
+
</figure>
|
|
87
|
+
|
|
88
|
+
<div class="person-text">
|
|
89
|
+
<span class="person-bar" aria-hidden="true"></span>
|
|
90
|
+
<div v-if="eyebrow" class="person-eyebrow">{{ eyebrow }}</div>
|
|
91
|
+
<h1 v-if="name" class="person-name">{{ name }}</h1>
|
|
92
|
+
<div v-if="role" class="person-role">{{ role }}</div>
|
|
93
|
+
<div class="person-bio"><slot /></div>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<!-- ────────── Duo-Modus ────────── -->
|
|
98
|
+
<div v-else class="person-duo">
|
|
99
|
+
<span class="person-bar duo-bar" aria-hidden="true"></span>
|
|
100
|
+
<div v-if="eyebrow" class="person-eyebrow duo-eyebrow">{{ eyebrow }}</div>
|
|
101
|
+
<div class="duo-lead"><slot /></div>
|
|
102
|
+
|
|
103
|
+
<div class="duo-grid">
|
|
104
|
+
<figure class="duo-card">
|
|
105
|
+
<div class="person-figure">
|
|
106
|
+
<img v-if="photo" :src="photoSrc" :alt="name" class="person-photo" @error="onPhotoError" />
|
|
107
|
+
<span v-else class="person-initials">{{ initials }}</span>
|
|
108
|
+
</div>
|
|
109
|
+
<h2 class="duo-name">{{ name }}</h2>
|
|
110
|
+
<div v-if="role" class="duo-role">{{ role }}</div>
|
|
111
|
+
</figure>
|
|
112
|
+
|
|
113
|
+
<figure class="duo-card">
|
|
114
|
+
<div class="person-figure">
|
|
115
|
+
<img v-if="photo2" :src="photo2Src" :alt="name2" class="person-photo" @error="onPhotoError" />
|
|
116
|
+
<span v-else class="person-initials">{{ initials2 }}</span>
|
|
117
|
+
</div>
|
|
118
|
+
<h2 class="duo-name">{{ name2 }}</h2>
|
|
119
|
+
<div v-if="role2" class="duo-role">{{ role2 }}</div>
|
|
120
|
+
</figure>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</template>
|
|
125
|
+
|
|
126
|
+
<style scoped>
|
|
127
|
+
.person-layout {
|
|
128
|
+
position: relative;
|
|
129
|
+
width: 100%;
|
|
130
|
+
height: 100%;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
background: var(--miragon-gray-bg);
|
|
133
|
+
color: var(--miragon-text-primary);
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
justify-content: center;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* --- Porträt-Rahmen (geteilt: Einzel + Duo) ------------------------------- */
|
|
140
|
+
.person-figure {
|
|
141
|
+
flex: 0 0 auto;
|
|
142
|
+
aspect-ratio: 4 / 5;
|
|
143
|
+
border-radius: 1.4rem;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
display: flex;
|
|
146
|
+
align-items: center;
|
|
147
|
+
justify-content: center;
|
|
148
|
+
background: var(--p-grad);
|
|
149
|
+
box-shadow: 0 18px 48px rgba(13, 13, 43, 0.18);
|
|
150
|
+
}
|
|
151
|
+
.person-photo {
|
|
152
|
+
width: 100%;
|
|
153
|
+
height: 100%;
|
|
154
|
+
object-fit: cover;
|
|
155
|
+
object-position: top center;
|
|
156
|
+
}
|
|
157
|
+
.person-initials {
|
|
158
|
+
font-weight: 800;
|
|
159
|
+
letter-spacing: -0.02em;
|
|
160
|
+
color: var(--miragon-white);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/* --- Akzent-Balken & Eyebrow (geteilt) ------------------------------------ */
|
|
164
|
+
.person-bar {
|
|
165
|
+
width: 3.5rem;
|
|
166
|
+
height: 0.35rem;
|
|
167
|
+
border-radius: 999px;
|
|
168
|
+
background: var(--p-grad);
|
|
169
|
+
margin-bottom: 1.5rem;
|
|
170
|
+
}
|
|
171
|
+
.person-eyebrow {
|
|
172
|
+
font-size: 0.9rem;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
letter-spacing: 0.18em;
|
|
175
|
+
text-transform: uppercase;
|
|
176
|
+
color: var(--p-role);
|
|
177
|
+
margin-bottom: 0.9rem;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ═══════════ Einzel-Modus ═══════════ */
|
|
181
|
+
.person-content {
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
gap: 4.5rem;
|
|
185
|
+
width: 100%;
|
|
186
|
+
max-width: 64rem;
|
|
187
|
+
padding: 0 5rem;
|
|
188
|
+
}
|
|
189
|
+
.side-right .person-content {
|
|
190
|
+
flex-direction: row-reverse;
|
|
191
|
+
}
|
|
192
|
+
.person-content .person-figure {
|
|
193
|
+
width: clamp(12rem, 21vw, 16.5rem);
|
|
194
|
+
}
|
|
195
|
+
.person-content .person-initials {
|
|
196
|
+
font-size: clamp(3rem, 6vw, 4.75rem);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.person-text {
|
|
200
|
+
display: flex;
|
|
201
|
+
flex-direction: column;
|
|
202
|
+
min-width: 0;
|
|
203
|
+
}
|
|
204
|
+
.person-name {
|
|
205
|
+
font-size: clamp(2.4rem, 4.4vw, 3.4rem);
|
|
206
|
+
line-height: 1.05;
|
|
207
|
+
font-weight: 800;
|
|
208
|
+
letter-spacing: -0.02em;
|
|
209
|
+
color: var(--miragon-text-primary);
|
|
210
|
+
margin: 0;
|
|
211
|
+
}
|
|
212
|
+
.person-role {
|
|
213
|
+
font-size: 1.3rem;
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
color: var(--p-role);
|
|
216
|
+
margin-top: 0.6rem;
|
|
217
|
+
}
|
|
218
|
+
.person-bio :deep(p) {
|
|
219
|
+
font-size: 1.15rem;
|
|
220
|
+
line-height: 1.6;
|
|
221
|
+
font-weight: 400;
|
|
222
|
+
color: var(--miragon-text-secondary);
|
|
223
|
+
margin: 1.5rem 0 0;
|
|
224
|
+
max-width: 34rem;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ═══════════ Duo-Modus ═══════════ */
|
|
228
|
+
.person-duo {
|
|
229
|
+
display: flex;
|
|
230
|
+
flex-direction: column;
|
|
231
|
+
align-items: center;
|
|
232
|
+
text-align: center;
|
|
233
|
+
width: 100%;
|
|
234
|
+
max-width: 60rem;
|
|
235
|
+
padding: 0 4rem;
|
|
236
|
+
}
|
|
237
|
+
.duo-bar {
|
|
238
|
+
margin-bottom: 1.25rem;
|
|
239
|
+
}
|
|
240
|
+
.duo-eyebrow {
|
|
241
|
+
margin-bottom: 0.5rem;
|
|
242
|
+
}
|
|
243
|
+
.duo-lead :deep(p) {
|
|
244
|
+
font-size: 1.15rem;
|
|
245
|
+
line-height: 1.55;
|
|
246
|
+
color: var(--miragon-text-secondary);
|
|
247
|
+
margin: 0 0 2.5rem;
|
|
248
|
+
max-width: 40rem;
|
|
249
|
+
}
|
|
250
|
+
.duo-lead :deep(p:only-child) {
|
|
251
|
+
margin-bottom: 2.5rem;
|
|
252
|
+
}
|
|
253
|
+
.duo-grid {
|
|
254
|
+
display: flex;
|
|
255
|
+
justify-content: center;
|
|
256
|
+
gap: 4rem;
|
|
257
|
+
width: 100%;
|
|
258
|
+
}
|
|
259
|
+
.duo-card {
|
|
260
|
+
flex: 1 1 0;
|
|
261
|
+
max-width: 20rem;
|
|
262
|
+
margin: 0;
|
|
263
|
+
display: flex;
|
|
264
|
+
flex-direction: column;
|
|
265
|
+
align-items: center;
|
|
266
|
+
}
|
|
267
|
+
.duo-card .person-figure {
|
|
268
|
+
width: clamp(8.5rem, 15vw, 12rem);
|
|
269
|
+
}
|
|
270
|
+
.duo-card .person-initials {
|
|
271
|
+
font-size: clamp(2.2rem, 4.5vw, 3.4rem);
|
|
272
|
+
}
|
|
273
|
+
.duo-name {
|
|
274
|
+
font-size: clamp(1.4rem, 2.6vw, 1.85rem);
|
|
275
|
+
line-height: 1.1;
|
|
276
|
+
font-weight: 800;
|
|
277
|
+
letter-spacing: -0.01em;
|
|
278
|
+
color: var(--miragon-text-primary);
|
|
279
|
+
margin: 1.4rem 0 0;
|
|
280
|
+
}
|
|
281
|
+
.duo-role {
|
|
282
|
+
font-size: 1.05rem;
|
|
283
|
+
font-weight: 600;
|
|
284
|
+
color: var(--p-role);
|
|
285
|
+
margin-top: 0.4rem;
|
|
286
|
+
}
|
|
287
|
+
</style>
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* section — Kapiteltrenner (STATISCHE Welt, KEIN Mesh-Shader).
|
|
4
|
+
*
|
|
5
|
+
* Strukturiert das Deck: markiert den Beginn eines neuen Kapitels. Ruhig wie
|
|
6
|
+
* `hero`, aber mit einer großen, halbtransparenten Index-Ziffer als
|
|
7
|
+
* Orientierungsanker im Hintergrund. Heller Grund, viel Weißraum, ein einziger
|
|
8
|
+
* Gradient-Akzent.
|
|
9
|
+
*
|
|
10
|
+
* Slots: default = Kapiteltitel (h1) + optionaler Einzeiler (p) aus Markdown.
|
|
11
|
+
* Frontmatter-Props:
|
|
12
|
+
* index — Kapitelnummer als String (z. B. "01"); rendert als Ghost-Ziffer
|
|
13
|
+
* eyebrow — kleine Überzeile (z. B. "Chapter")
|
|
14
|
+
* accent — "blue" | "green" | "mixed" — Gradient-Akzent (default blue)
|
|
15
|
+
*/
|
|
16
|
+
import { computed } from 'vue'
|
|
17
|
+
|
|
18
|
+
const props = withDefaults(
|
|
19
|
+
defineProps<{
|
|
20
|
+
index?: string
|
|
21
|
+
eyebrow?: string
|
|
22
|
+
accent?: 'blue' | 'green' | 'mixed'
|
|
23
|
+
}>(),
|
|
24
|
+
{ accent: 'blue' },
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
// Token reaktiv (siehe hero.vue) — alle Werte aus theme.css, keine Hex.
|
|
28
|
+
const gradientVar = computed(() => `var(--miragon-gradient-${props.accent})`)
|
|
29
|
+
const accentVar = computed(() =>
|
|
30
|
+
props.accent === 'green' ? 'var(--miragon-green-deep)' : 'var(--miragon-blue)',
|
|
31
|
+
)
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<template>
|
|
35
|
+
<div
|
|
36
|
+
class="section-layout"
|
|
37
|
+
:style="{ '--s-grad': gradientVar, '--s-accent': accentVar }"
|
|
38
|
+
>
|
|
39
|
+
<span v-if="index" class="section-ghost" aria-hidden="true">{{ index }}</span>
|
|
40
|
+
|
|
41
|
+
<div class="section-content">
|
|
42
|
+
<span class="section-bar" aria-hidden="true"></span>
|
|
43
|
+
<div v-if="eyebrow" class="section-eyebrow">{{ eyebrow }}</div>
|
|
44
|
+
<div class="section-body">
|
|
45
|
+
<slot />
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<style scoped>
|
|
52
|
+
.section-layout {
|
|
53
|
+
position: relative;
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
overflow: hidden;
|
|
57
|
+
background: var(--miragon-gray-bg);
|
|
58
|
+
color: var(--miragon-text-primary);
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/* Große Ghost-Ziffer als Hintergrund-Anker — rein dekorativ, sehr dezent.
|
|
64
|
+
BEWUSST kein background-clip:text-Gradient (headless Chromium beim PDF-Export
|
|
65
|
+
rendert clip-Schrift unsichtbar, vgl. hero.vue) — solide Akzentfarbe mit
|
|
66
|
+
geringer Deckkraft ist exportsicher. */
|
|
67
|
+
.section-ghost {
|
|
68
|
+
position: absolute;
|
|
69
|
+
z-index: 0;
|
|
70
|
+
right: 4rem;
|
|
71
|
+
bottom: 1.5rem;
|
|
72
|
+
font-size: 24rem;
|
|
73
|
+
line-height: 1;
|
|
74
|
+
font-weight: 900;
|
|
75
|
+
letter-spacing: -0.04em;
|
|
76
|
+
color: var(--s-accent);
|
|
77
|
+
opacity: 0.07;
|
|
78
|
+
user-select: none;
|
|
79
|
+
pointer-events: none;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.section-content {
|
|
83
|
+
position: relative;
|
|
84
|
+
z-index: 1;
|
|
85
|
+
width: 100%;
|
|
86
|
+
max-width: 60rem;
|
|
87
|
+
padding: 0 5rem;
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.section-bar {
|
|
93
|
+
width: 4.5rem;
|
|
94
|
+
height: 0.4rem;
|
|
95
|
+
border-radius: 999px;
|
|
96
|
+
background: var(--s-grad);
|
|
97
|
+
margin-bottom: 2rem;
|
|
98
|
+
}
|
|
99
|
+
.section-eyebrow {
|
|
100
|
+
font-size: 0.95rem;
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
letter-spacing: 0.18em;
|
|
103
|
+
text-transform: uppercase;
|
|
104
|
+
color: var(--s-accent);
|
|
105
|
+
margin-bottom: 1.25rem;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.section-body :deep(h1) {
|
|
109
|
+
font-size: clamp(2.8rem, 5vw, 4.2rem);
|
|
110
|
+
line-height: 1.08;
|
|
111
|
+
font-weight: 800;
|
|
112
|
+
letter-spacing: -0.02em;
|
|
113
|
+
color: var(--miragon-text-primary);
|
|
114
|
+
margin: 0;
|
|
115
|
+
}
|
|
116
|
+
.section-body :deep(h1 strong) {
|
|
117
|
+
font-weight: 800;
|
|
118
|
+
color: var(--s-accent);
|
|
119
|
+
}
|
|
120
|
+
.section-body :deep(p) {
|
|
121
|
+
font-size: 1.2rem;
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
color: var(--miragon-text-muted);
|
|
124
|
+
margin: 1.75rem 0 0;
|
|
125
|
+
max-width: 38rem;
|
|
126
|
+
}
|
|
127
|
+
</style>
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* showcase — interactive feature explorer (STATIC world, no shader).
|
|
4
|
+
*
|
|
5
|
+
* A row of clickable cards; one is active at a time. Click any card to switch
|
|
6
|
+
* the active item; the detail panel below cross-fades to the new content.
|
|
7
|
+
* Demonstrates Vue reactivity, CSS transitions, and Vue components living
|
|
8
|
+
* inside a slide — beyond static Markdown.
|
|
9
|
+
*
|
|
10
|
+
* Items are passed as a YAML array via frontmatter; the layout iterates and
|
|
11
|
+
* the user clicks to explore. Default state: first item active.
|
|
12
|
+
*
|
|
13
|
+
* Frontmatter props:
|
|
14
|
+
* title — slide title (h2-level)
|
|
15
|
+
* eyebrow — uppercase kicker
|
|
16
|
+
* accent — "blue" | "green" | "mixed" (default mixed)
|
|
17
|
+
* items — array of `{ label, body }` objects (recommended: 3–4 cards)
|
|
18
|
+
*/
|
|
19
|
+
import { computed, ref, watch } from 'vue'
|
|
20
|
+
|
|
21
|
+
interface Item {
|
|
22
|
+
label: string
|
|
23
|
+
body: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(
|
|
27
|
+
defineProps<{
|
|
28
|
+
eyebrow?: string
|
|
29
|
+
accent?: 'blue' | 'green' | 'mixed'
|
|
30
|
+
items?: Item[]
|
|
31
|
+
frontmatter?: Record<string, unknown>
|
|
32
|
+
}>(),
|
|
33
|
+
{ accent: 'mixed', items: () => [] },
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const title = computed(() => props.frontmatter?.title as string | undefined)
|
|
37
|
+
const gradientVar = computed(() => `var(--miragon-gradient-${props.accent})`)
|
|
38
|
+
const accentVar = computed(() =>
|
|
39
|
+
props.accent === 'green' ? 'var(--miragon-green-deep)' : 'var(--miragon-blue)',
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const selected = ref(0)
|
|
43
|
+
const activeItem = computed(() => props.items[selected.value])
|
|
44
|
+
|
|
45
|
+
// Defensive: clamp selection if the items list shrinks below the current
|
|
46
|
+
// index (e.g. when the slide is re-entered with a different array).
|
|
47
|
+
watch(
|
|
48
|
+
() => props.items.length,
|
|
49
|
+
(len) => {
|
|
50
|
+
if (selected.value >= len) selected.value = 0
|
|
51
|
+
},
|
|
52
|
+
)
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<template>
|
|
56
|
+
<div class="showcase-layout" :style="{ '--sc-grad': gradientVar, '--sc-accent': accentVar }">
|
|
57
|
+
<div class="showcase-inner">
|
|
58
|
+
<header v-if="title || eyebrow" class="showcase-head">
|
|
59
|
+
<span class="showcase-bar" aria-hidden="true"></span>
|
|
60
|
+
<div v-if="eyebrow" class="showcase-eyebrow">{{ eyebrow }}</div>
|
|
61
|
+
<h2 v-if="title" class="showcase-title">{{ title }}</h2>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<div class="showcase-grid" :class="`cols-${items.length}`">
|
|
65
|
+
<button
|
|
66
|
+
v-for="(item, i) in items"
|
|
67
|
+
:key="i"
|
|
68
|
+
type="button"
|
|
69
|
+
class="showcase-card"
|
|
70
|
+
:class="{ 'is-active': i === selected }"
|
|
71
|
+
@click="selected = i"
|
|
72
|
+
>
|
|
73
|
+
<span class="card-index">{{ String(i + 1).padStart(2, '0') }}</span>
|
|
74
|
+
<span class="card-label">{{ item.label }}</span>
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="showcase-detail">
|
|
79
|
+
<transition name="fade-detail" mode="out-in">
|
|
80
|
+
<p :key="selected" class="detail-body">{{ activeItem?.body }}</p>
|
|
81
|
+
</transition>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<p class="showcase-hint" aria-hidden="true">Click a card to switch.</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
|
|
89
|
+
<style scoped>
|
|
90
|
+
.showcase-layout {
|
|
91
|
+
position: relative;
|
|
92
|
+
width: 100%;
|
|
93
|
+
height: 100%;
|
|
94
|
+
overflow: hidden;
|
|
95
|
+
background: var(--miragon-gray-bg);
|
|
96
|
+
color: var(--miragon-text-primary);
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: stretch;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.showcase-inner {
|
|
102
|
+
position: relative;
|
|
103
|
+
z-index: 1;
|
|
104
|
+
width: 100%;
|
|
105
|
+
max-width: 72rem;
|
|
106
|
+
margin: 0 auto;
|
|
107
|
+
padding: 3rem 4rem 2.5rem;
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.showcase-head {
|
|
113
|
+
flex: 0 0 auto;
|
|
114
|
+
margin-bottom: 2rem;
|
|
115
|
+
}
|
|
116
|
+
.showcase-bar {
|
|
117
|
+
display: block;
|
|
118
|
+
width: 3.5rem;
|
|
119
|
+
height: 0.35rem;
|
|
120
|
+
border-radius: 999px;
|
|
121
|
+
background: var(--sc-grad);
|
|
122
|
+
margin-bottom: 1rem;
|
|
123
|
+
}
|
|
124
|
+
.showcase-eyebrow {
|
|
125
|
+
font-size: 0.9rem;
|
|
126
|
+
font-weight: 600;
|
|
127
|
+
letter-spacing: 0.18em;
|
|
128
|
+
text-transform: uppercase;
|
|
129
|
+
color: var(--sc-accent);
|
|
130
|
+
margin-bottom: 0.6rem;
|
|
131
|
+
}
|
|
132
|
+
.showcase-title {
|
|
133
|
+
font-size: clamp(1.7rem, 2.7vw, 2.2rem);
|
|
134
|
+
line-height: 1.15;
|
|
135
|
+
font-weight: 800;
|
|
136
|
+
letter-spacing: -0.02em;
|
|
137
|
+
margin: 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.showcase-grid {
|
|
141
|
+
flex: 0 0 auto;
|
|
142
|
+
display: grid;
|
|
143
|
+
gap: 1rem;
|
|
144
|
+
margin-bottom: 1.75rem;
|
|
145
|
+
}
|
|
146
|
+
.showcase-grid.cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
147
|
+
.showcase-grid.cols-4 { grid-template-columns: repeat(4, 1fr); }
|
|
148
|
+
.showcase-grid.cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
149
|
+
|
|
150
|
+
.showcase-card {
|
|
151
|
+
position: relative;
|
|
152
|
+
text-align: left;
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
background: var(--miragon-white);
|
|
155
|
+
border: 1px solid #E5E7EB;
|
|
156
|
+
border-radius: 1rem;
|
|
157
|
+
padding: 1.1rem 1.2rem 1.2rem;
|
|
158
|
+
box-shadow: 0 8px 20px rgba(51, 93, 229, 0.08);
|
|
159
|
+
font: inherit;
|
|
160
|
+
color: inherit;
|
|
161
|
+
display: flex;
|
|
162
|
+
flex-direction: column;
|
|
163
|
+
gap: 0.55rem;
|
|
164
|
+
transition:
|
|
165
|
+
transform 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
|
166
|
+
box-shadow 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
|
167
|
+
border-color 320ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
|
168
|
+
opacity 320ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
169
|
+
}
|
|
170
|
+
.showcase-card:focus-visible {
|
|
171
|
+
outline: 2px solid var(--sc-accent);
|
|
172
|
+
outline-offset: 3px;
|
|
173
|
+
}
|
|
174
|
+
.showcase-card:hover {
|
|
175
|
+
transform: translateY(-2px);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.card-index {
|
|
179
|
+
font-size: 0.7rem;
|
|
180
|
+
font-weight: 700;
|
|
181
|
+
letter-spacing: 0.14em;
|
|
182
|
+
text-transform: uppercase;
|
|
183
|
+
color: var(--miragon-text-muted);
|
|
184
|
+
transition: color 320ms ease;
|
|
185
|
+
}
|
|
186
|
+
.card-label {
|
|
187
|
+
font-size: 1.05rem;
|
|
188
|
+
font-weight: 700;
|
|
189
|
+
color: var(--miragon-text-primary);
|
|
190
|
+
letter-spacing: -0.005em;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.showcase-card.is-active {
|
|
194
|
+
transform: translateY(-4px) scale(1.02);
|
|
195
|
+
border-color: var(--sc-accent);
|
|
196
|
+
box-shadow: 0 16px 36px rgba(51, 93, 229, 0.18);
|
|
197
|
+
}
|
|
198
|
+
.showcase-card.is-active .card-index {
|
|
199
|
+
color: var(--sc-accent);
|
|
200
|
+
}
|
|
201
|
+
.showcase-card:not(.is-active) {
|
|
202
|
+
opacity: 0.55;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.showcase-detail {
|
|
206
|
+
flex: 1 1 auto;
|
|
207
|
+
min-height: 5rem;
|
|
208
|
+
background: var(--miragon-white);
|
|
209
|
+
border: 1px solid #E5E7EB;
|
|
210
|
+
border-radius: 1rem;
|
|
211
|
+
padding: 1.25rem 1.5rem;
|
|
212
|
+
box-shadow: 0 8px 20px rgba(51, 93, 229, 0.08);
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
position: relative;
|
|
216
|
+
}
|
|
217
|
+
.detail-body {
|
|
218
|
+
font-size: 1.1rem;
|
|
219
|
+
line-height: 1.55;
|
|
220
|
+
color: var(--miragon-text-secondary);
|
|
221
|
+
margin: 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Cross-fade for the detail panel content when the user picks another card. */
|
|
225
|
+
.fade-detail-enter-active,
|
|
226
|
+
.fade-detail-leave-active {
|
|
227
|
+
transition: opacity 220ms ease, transform 220ms ease;
|
|
228
|
+
}
|
|
229
|
+
.fade-detail-enter-from,
|
|
230
|
+
.fade-detail-leave-to {
|
|
231
|
+
opacity: 0;
|
|
232
|
+
transform: translateY(6px);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.showcase-hint {
|
|
236
|
+
flex: 0 0 auto;
|
|
237
|
+
margin: 0.9rem 0 0;
|
|
238
|
+
font-size: 0.74rem;
|
|
239
|
+
letter-spacing: 0.12em;
|
|
240
|
+
text-transform: uppercase;
|
|
241
|
+
color: var(--miragon-text-muted);
|
|
242
|
+
text-align: center;
|
|
243
|
+
}
|
|
244
|
+
</style>
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@miragon/slidev-toolkit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Miragon Slidev toolkit: the brand theme, the layout archetypes, the reusable components (Card, CardGrid, StepList, Figure, SplitView) and the BrandMeshBackground animation. Single source of design truth, consumed by the reference deck in this repo.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"slidev-theme",
|
|
7
|
+
"slidev",
|
|
8
|
+
"miragon",
|
|
9
|
+
"toolkit"
|
|
10
|
+
],
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Miragon/slidev-deck-template.git",
|
|
15
|
+
"directory": "packages/toolkit"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/Miragon/slidev-deck-template/tree/main/packages/toolkit#readme",
|
|
18
|
+
"bugs": "https://github.com/Miragon/slidev-deck-template/issues",
|
|
19
|
+
"engines": {
|
|
20
|
+
"slidev": ">=0.48.0"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@paper-design/shaders": "0.0.77"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"vue": "^3.5"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"layouts",
|
|
30
|
+
"components",
|
|
31
|
+
"styles",
|
|
32
|
+
"assets"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public",
|
|
36
|
+
"provenance": true
|
|
37
|
+
}
|
|
38
|
+
}
|