@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.
@@ -0,0 +1,159 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * BrandMeshBackground — animierter WebGL2 Mesh-Gradient-Hintergrund (Miragon CI).
4
+ *
5
+ * HEILIG: Farbstops und Parameter stammen 1:1 aus dem bestehenden
6
+ * @miragon/slidev-theme. Nicht ohne Rücksprache ändern.
7
+ *
8
+ * Verwendung: liegt als unterste Ebene (z-index 0) in den animierten
9
+ * Layouts (cover, closing). pointer-events:none, damit Klicks durchgehen.
10
+ * Fallback-Navy #0d0d2b für den ersten Frame / kein WebGL.
11
+ */
12
+ import { onMounted, onBeforeUnmount, ref } from 'vue'
13
+ import {
14
+ ShaderMount,
15
+ meshGradientFragmentShader,
16
+ getShaderColorFromString,
17
+ defaultObjectSizing,
18
+ ShaderFitOptions,
19
+ } from '@paper-design/shaders'
20
+
21
+ // --- Heilige Markenparameter (DO NOT CHANGE) -------------------------------
22
+ const COLORS = ['#335DE5', '#1a1a4e', '#00E676', '#335DE5', '#0d0d2b', '#00C853']
23
+ const DISTORTION = 1 // Verformung der Farbfelder
24
+ const SWIRL = 0.5 // Verwirbelung der Übergänge
25
+ const SPEED = 0.6 // Animationsgeschwindigkeit
26
+ // ---------------------------------------------------------------------------
27
+
28
+ const host = ref<HTMLDivElement | null>(null)
29
+ let mount: ShaderMount | null = null
30
+
31
+ onMounted(() => {
32
+ if (!host.value) return
33
+ try {
34
+ // Sizing/Fit-Uniforms explizit übergeben (sonst nutzt der Shader Default-
35
+ // Werte, die je nach Slide-Format zu inkonsistentem Crop/Scale führen).
36
+ const uniforms = {
37
+ u_colors: COLORS.map((c) => getShaderColorFromString(c)),
38
+ u_colorsCount: COLORS.length,
39
+ u_distortion: DISTORTION,
40
+ u_swirl: SWIRL,
41
+ u_grainMixer: 0,
42
+ u_grainOverlay: 0,
43
+ u_fit: ShaderFitOptions[defaultObjectSizing.fit],
44
+ u_rotation: defaultObjectSizing.rotation,
45
+ u_scale: defaultObjectSizing.scale,
46
+ u_offsetX: defaultObjectSizing.offsetX,
47
+ u_offsetY: defaultObjectSizing.offsetY,
48
+ u_originX: defaultObjectSizing.originX,
49
+ u_originY: defaultObjectSizing.originY,
50
+ u_worldWidth: defaultObjectSizing.worldWidth,
51
+ u_worldHeight: defaultObjectSizing.worldHeight,
52
+ }
53
+ // Performance (rührt die heiligen COLORS/DISTORTION/SWIRL/SPEED NICHT an):
54
+ // die Library-Defaults erzwingen minPixelRatio=2 und cappen bei 1920*1080*4
55
+ // (~8.3M) Pixeln, d. h. auf einem Retina-Display läuft dieser teure Mesh-
56
+ // Gradient-Fragment-Shader über ~8.3M Pixel pro Frame und ruckelt. Ein weicher
57
+ // Verlauf braucht keinen 4K-Backing-Store: Supersampling weg (minPixelRatio 1)
58
+ // und den Render auf ~0.9M Pixel cappen. CSS skaliert das Canvas hoch, der Blur
59
+ // kaschiert es.
60
+ const MIN_PIXEL_RATIO = 1
61
+ const MAX_PIXEL_COUNT = 640 * 360
62
+ // Reduced-Motion respektieren: auf dem ersten Frame einfrieren (speed 0)
63
+ // statt zu animieren — kostet ebenfalls nichts.
64
+ const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false
65
+ mount = new ShaderMount(
66
+ host.value,
67
+ meshGradientFragmentShader,
68
+ uniforms,
69
+ undefined,
70
+ reduceMotion ? 0 : SPEED,
71
+ 0,
72
+ MIN_PIXEL_RATIO,
73
+ MAX_PIXEL_COUNT,
74
+ )
75
+ // WebGL läuft: den blurred, dauerhaft animierten CSS-Fallback-Layer abschalten.
76
+ // Er liegt unsichtbar hinter dem nun opaken Canvas, kostet aber pro Frame
77
+ // Compositor-Zeit (ein blur(48px)-Layer in doppelter Viewport-Größe), was die
78
+ // FPS senkt und das Canvas während Slide-Transitions desynchronisiert.
79
+ host.value.classList.add('webgl-active')
80
+ } catch (e) {
81
+ // WebGL nicht verfügbar (z. B. headless) → Fallback-Navy bleibt sichtbar.
82
+ console.warn('[BrandMeshBackground] WebGL unavailable, falling back to solid navy.', e)
83
+ }
84
+ })
85
+
86
+ onBeforeUnmount(() => {
87
+ mount?.dispose()
88
+ mount = null
89
+ })
90
+ </script>
91
+
92
+ <template>
93
+ <div ref="host" class="mesh-bg" aria-hidden="true"></div>
94
+ </template>
95
+
96
+ <style scoped>
97
+ .mesh-bg {
98
+ position: absolute;
99
+ inset: 0;
100
+ width: 100%;
101
+ height: 100%;
102
+ background: #0d0d2b; /* Navy-Basis für allerersten Frame */
103
+ pointer-events: none;
104
+ z-index: 0;
105
+ overflow: hidden; /* hält den überdimensionierten Fallback-Layer im Rahmen */
106
+ }
107
+
108
+ /*
109
+ * CSS-Fallback (additiv, ändert NICHT die heiligen Shader-Werte):
110
+ * Animierter Mesh-Verlauf in den Markenfarben. Liegt HINTER dem Canvas.
111
+ * - WebGL ok → ShaderMount-Canvas ist opak und überdeckt diesen Layer.
112
+ * - WebGL fehlt (Browser ohne GPU, `npm run export`/headless) → der Canvas
113
+ * bleibt transparent, dieser Layer scheint durch und animiert weiter.
114
+ * Farben 1:1 aus der Mesh-Palette (Blau/Navy/Grün), kein totes Navy mehr.
115
+ *
116
+ * z-index: -2 → liegt HINTER dem Library-Canvas (der bei WebGL-Erfolg per
117
+ * `@layer paper-shaders` z-index:-1 bekommt). So überdeckt der echte Shader
118
+ * diesen Fallback; bei WebGL-Ausfall (transparenter/leerer Canvas) scheint er
119
+ * über der Navy-Basis durch.
120
+ */
121
+ .mesh-bg::before {
122
+ content: '';
123
+ position: absolute;
124
+ z-index: -2;
125
+ inset: -50%;
126
+ background:
127
+ radial-gradient(38% 38% at 24% 30%, #335DE5 0%, transparent 60%),
128
+ radial-gradient(34% 34% at 76% 22%, #00E676 0%, transparent 60%),
129
+ radial-gradient(44% 44% at 70% 76%, #00C853 0%, transparent 60%),
130
+ radial-gradient(40% 40% at 28% 80%, #1a1a4e 0%, transparent 62%),
131
+ radial-gradient(55% 55% at 50% 50%, #335DE5 0%, transparent 72%);
132
+ filter: blur(48px);
133
+ will-change: transform;
134
+ animation: mesh-fallback-float 22s ease-in-out infinite alternate;
135
+ }
136
+
137
+ @keyframes mesh-fallback-float {
138
+ 0% { transform: translate(-4%, -3%) scale(1.06) rotate(0deg); }
139
+ 50% { transform: translate(3%, 2%) scale(1.16) rotate(7deg); }
140
+ 100% { transform: translate(2%, -4%) scale(1.10) rotate(-6deg); }
141
+ }
142
+
143
+ @media (prefers-reduced-motion: reduce) {
144
+ .mesh-bg::before { animation: none; }
145
+ }
146
+
147
+ /* WebGL erfolgreich: das opake Shader-Canvas überdeckt den Fallback, also den
148
+ animierten Blur-Layer ganz weglassen (kein verschwendetes Compositing). */
149
+ .mesh-bg.webgl-active::before {
150
+ display: none;
151
+ }
152
+
153
+ /* Canvas-Positionierung der Library NICHT überschreiben (sie setzt position
154
+ absolute + z-index:-1 via @layer). Nur die Größe absichern. */
155
+ .mesh-bg canvas {
156
+ width: 100% !important;
157
+ height: 100% !important;
158
+ }
159
+ </style>
@@ -0,0 +1,72 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Card — kanonische weiße Brand-Card (Design-System §6).
4
+ * Hintergrund IMMER weiß, Akzent NUR auf dem Titel. `accent` wählt einen Stop
5
+ * der blue → teal → green-Progression (§6.3) — dies ist der einzige
6
+ * sanktionierte Ort für rohe Hex-Werte, hier zentral statt im Deck verstreut.
7
+ *
8
+ * Mehrkartige Grids: Titel von links nach rechts entlang der Progression.
9
+ * 2 Karten: blue, green
10
+ * 3 Karten: blue, teal, green
11
+ * 4 Karten: blue, blue-mid, green-deep, green
12
+ * <Card title="Deployment" accent="blue">Body …</Card>
13
+ */
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ title?: string
17
+ accent?: 'blue' | 'blue-mid' | 'teal' | 'green-deep' | 'green-mid' | 'green'
18
+ padding?: 'compact' | 'standard' | 'generous'
19
+ }>(),
20
+ { accent: 'blue', padding: 'standard' },
21
+ )
22
+
23
+ // Die §6.3-Progressions-Stops. Bewusst lokal: der einzige sanktionierte Hex-Ort.
24
+ const ACCENTS: Record<string, string> = {
25
+ blue: '#335DE5',
26
+ 'blue-mid': '#2B5ACE',
27
+ teal: '#1E7A8A',
28
+ 'green-deep': '#0E8E6E',
29
+ 'green-mid': '#00974F',
30
+ green: '#00C263',
31
+ }
32
+ </script>
33
+
34
+ <template>
35
+ <div class="mg-card" :class="`mg-card--${props.padding}`">
36
+ <h3 v-if="title" class="mg-card__title" :style="{ color: ACCENTS[props.accent] }">{{ title }}</h3>
37
+ <div class="mg-card__body"><slot /></div>
38
+ </div>
39
+ </template>
40
+
41
+ <style scoped>
42
+ .mg-card {
43
+ border-radius: 0.75rem;
44
+ border: 1px solid #e5e7eb;
45
+ background: var(--miragon-white);
46
+ box-shadow: 0 8px 20px rgba(51, 93, 229, 0.08);
47
+ }
48
+ /* Nur Karten mit Zeichnung (compact) werden zur Flex-Spalte: im Grid stretchen
49
+ die Karten auf gleiche Höhe, eine abschließende Zeichnung (margin-top:auto)
50
+ dockt am Kartenboden an und liegt über alle Karten hinweg auf gleicher Höhe.
51
+ Standard-Karten bleiben normaler Fluss, sonst würde inline <strong> im Body
52
+ zu eigenen Flex-Items (eigene Zeilen) umbrechen. */
53
+ .mg-card--compact { padding: 1rem; display: flex; flex-direction: column; }
54
+ .mg-card--standard { padding: 1.25rem; }
55
+ .mg-card--generous { padding: 1.5rem; }
56
+ .mg-card__title {
57
+ margin: 0;
58
+ font-weight: 700;
59
+ font-size: 1.05rem;
60
+ line-height: 1.2;
61
+ }
62
+ .mg-card__body {
63
+ margin-top: 0.5rem;
64
+ font-size: 0.875rem;
65
+ line-height: 1.55;
66
+ color: #4b5563;
67
+ }
68
+ .mg-card--compact .mg-card__body { flex: 1 1 auto; display: flex; flex-direction: column; }
69
+ /* A trailing diagram (<Figure src>) docks at the card bottom, so the image sits at
70
+ the same position in every card of a stretched grid regardless of text length. */
71
+ .mg-card--compact .mg-card__body :deep(.mg-figure) { margin-top: auto; }
72
+ </style>
@@ -0,0 +1,46 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CardGrid — gleichmaessiges Spalten-Raster fuer Karten/Figuren (Design-System §6).
4
+ * Ersetzt das rohe `<div class="grid grid-cols-N gap-…">` im Markdown, damit Folien
5
+ * keine Utility-Klassen tragen: nur semantische Tags. Karten im Grid strecken auf
6
+ * gleiche Hoehe (align: stretch). `cols` setzt die Spaltenzahl, `gap` den Abstand.
7
+ *
8
+ * <CardGrid cols="3">
9
+ * <Card title="…" accent="blue">…</Card>
10
+ * <Card title="…" accent="teal">…</Card>
11
+ * <Card title="…" accent="green">…</Card>
12
+ * </CardGrid>
13
+ */
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ cols?: number | string
17
+ gap?: 'compact' | 'standard' | 'generous'
18
+ }>(),
19
+ { cols: 3, gap: 'standard' },
20
+ )
21
+
22
+ const GAPS: Record<string, string> = {
23
+ compact: '1rem',
24
+ standard: '1.5rem',
25
+ generous: '2rem',
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <div
31
+ class="mg-card-grid"
32
+ :style="{ '--mg-cols': String(props.cols), '--mg-gap': GAPS[props.gap] }"
33
+ >
34
+ <slot />
35
+ </div>
36
+ </template>
37
+
38
+ <style scoped>
39
+ .mg-card-grid {
40
+ display: grid;
41
+ grid-template-columns: repeat(var(--mg-cols), minmax(0, 1fr));
42
+ gap: var(--mg-gap);
43
+ align-items: stretch;
44
+ margin-top: 0.5rem;
45
+ }
46
+ </style>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+
4
+ /**
5
+ * Figure — beschriftete Diagramm-Spalte: Titel oben, Grafik (Slot) mittig,
6
+ * Caption unten. Ersetzt die handgestylten text-center-<div>/<p>-Paare
7
+ * (z. B. der VM/Container/Pod-Vergleich).
8
+ * <Figure title="Container" caption="Container teilen sich den **Host-Kernel**.">
9
+ * <svg …></svg>
10
+ * </Figure>
11
+ * Die Caption versteht Inline-Markdown: **fett**, *kursiv*, `code`.
12
+ *
13
+ * Statt eines Slot-Visuals kann auch ein Public-Asset per `src` eingebunden
14
+ * werden (z. B. eine .excalidraw.png). Der Pfad wird base-aware über
15
+ * BASE_URL aufgelöst — KEIN Markdown-Bild verwenden: Slidev kompiliert ein
16
+ * `![](…)` in einen Vite-Import, der bei Public-Assets den Build bricht.
17
+ * <Figure src="images/foo.excalidraw.png" alt="…" caption="…"></Figure>
18
+ */
19
+ const props = withDefaults(
20
+ defineProps<{
21
+ title?: string
22
+ caption?: string
23
+ src?: string
24
+ alt?: string
25
+ maxHeight?: string
26
+ }>(),
27
+ { maxHeight: '340px' },
28
+ )
29
+
30
+ // Assets werden unter BASE_URL serviert (z. B. "/" lokal, "/<repo>/" auf GitHub
31
+ // Pages) — etwa Kapitel-Ressourcen unter /resources/<chapter>/<file>. Slash normalisieren.
32
+ const resolvedSrc = computed(() => {
33
+ if (!props.src) return ''
34
+ const base = import.meta.env.BASE_URL.replace(/\/$/, '')
35
+ return `${base}/${props.src.replace(/^\//, '')}`
36
+ })
37
+
38
+ // HTML-sicher: erst escapen, dann nur die erlaubten Inline-Marker zu Tags machen.
39
+ const captionHtml = computed(() => {
40
+ if (!props.caption) return ''
41
+ const escaped = props.caption
42
+ .replace(/&/g, '&amp;')
43
+ .replace(/</g, '&lt;')
44
+ .replace(/>/g, '&gt;')
45
+ return escaped
46
+ .replace(/`([^`]+)`/g, '<code>$1</code>')
47
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
48
+ .replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>')
49
+ })
50
+ </script>
51
+
52
+ <template>
53
+ <div class="mg-figure">
54
+ <div v-if="title" class="mg-figure__title">{{ title }}</div>
55
+ <div class="mg-figure__media">
56
+ <img v-if="resolvedSrc" :src="resolvedSrc" :alt="alt" :style="{ maxHeight }" />
57
+ <slot v-else />
58
+ </div>
59
+ <div v-if="caption" class="mg-figure__caption" v-html="captionHtml"></div>
60
+ </div>
61
+ </template>
62
+
63
+ <style scoped>
64
+ .mg-figure {
65
+ display: flex;
66
+ flex-direction: column;
67
+ }
68
+ .mg-figure__title {
69
+ text-align: center;
70
+ font-weight: 700;
71
+ font-size: 1rem;
72
+ color: var(--miragon-text-primary);
73
+ margin-bottom: 0.5rem;
74
+ }
75
+ .mg-figure__media {
76
+ display: flex;
77
+ justify-content: center;
78
+ }
79
+ .mg-figure__media img {
80
+ max-width: 100%;
81
+ height: auto;
82
+ }
83
+ .mg-figure__caption {
84
+ text-align: center;
85
+ color: var(--miragon-text-secondary);
86
+ line-height: 1.4;
87
+ font-size: 0.8rem;
88
+ margin-top: 1.5rem;
89
+ }
90
+ .mg-figure__caption :deep(strong) {
91
+ font-weight: 700;
92
+ color: var(--miragon-text-primary);
93
+ }
94
+ .mg-figure__caption :deep(em) {
95
+ font-style: italic;
96
+ }
97
+ .mg-figure__caption :deep(code) {
98
+ font-family: var(--miragon-font-mono, ui-monospace, monospace);
99
+ font-size: 0.92em;
100
+ }
101
+ </style>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SplitView — zweispaltiges „Visual + Erklärung"-Raster: links eine Grafik
4
+ * (Diagramm-Komponente oder Bild über den #visual-Slot), rechts der erklärende
5
+ * Text im Default-Slot (Bullets oder <StepList>). Ersetzt die handgestylten
6
+ * <div class="grid grid-cols-2 …">-Wrapper in slides.md und vereinheitlicht
7
+ * Spaltenverhältnis, Abstand und vertikale Ausrichtung.
8
+ *
9
+ * Beide Spalten sind standardmäßig vertikal zentriert (align="center"), damit
10
+ * die Stichpunkte mittig zur Grafik sitzen.
11
+ *
12
+ * Props:
13
+ * ratio — Spaltenverhältnis links/rechts als "a/b" (Default "1/1";
14
+ * z. B. "1.5/1" für eine breitere Grafik)
15
+ * align — vertikale Ausrichtung der Spalten: "center" | "start" | "end" |
16
+ * "stretch" (Default "center")
17
+ * gap — Spaltenabstand als CSS-Länge (Default "2.5rem")
18
+ *
19
+ * Verwendung:
20
+ * <SplitView ratio="1.5/1">
21
+ * <template #visual>
22
+ * <RuntimeView></RuntimeView>
23
+ * </template>
24
+ *
25
+ * - Punkt eins
26
+ * - Punkt zwei
27
+ * </SplitView>
28
+ *
29
+ * Styling lebt ausschließlich hier; die Bullet-Marker kommen weiterhin vom
30
+ * content-Layout, nie aus diesem Component.
31
+ */
32
+ import { computed } from 'vue'
33
+
34
+ const props = withDefaults(
35
+ defineProps<{
36
+ ratio?: string
37
+ align?: 'center' | 'start' | 'end' | 'stretch'
38
+ gap?: string
39
+ }>(),
40
+ { ratio: '1/1', align: 'center', gap: '2.5rem' },
41
+ )
42
+
43
+ const columns = computed(() => {
44
+ const parts = props.ratio.split('/').map((n) => n.trim() || '1')
45
+ const left = parts[0] || '1'
46
+ const right = parts[1] || '1'
47
+ return `${left}fr ${right}fr`
48
+ })
49
+ </script>
50
+
51
+ <template>
52
+ <div
53
+ class="mg-split"
54
+ :style="{ gridTemplateColumns: columns, alignItems: align, gap }"
55
+ >
56
+ <div class="mg-split__visual"><slot name="visual" /></div>
57
+ <div class="mg-split__body"><slot /></div>
58
+ </div>
59
+ </template>
60
+
61
+ <style scoped>
62
+ .mg-split {
63
+ display: grid;
64
+ margin-top: 0.5rem;
65
+ }
66
+ /* min-width:0 verhindert, dass breite Grafiken die Spalte über fr sprengen. */
67
+ .mg-split__visual,
68
+ .mg-split__body {
69
+ min-width: 0;
70
+ }
71
+ </style>
@@ -0,0 +1,19 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Step — eine Zeile in einer <StepList>: fett gesetztes Label + Fließtext.
4
+ * Das Label nutzt die primäre Textfarbe, der Body erbt die Farbe der StepList.
5
+ * <Step label="Client">ruft … auf</Step>
6
+ */
7
+ defineProps<{ label?: string }>()
8
+ </script>
9
+
10
+ <template>
11
+ <div class="step"><strong v-if="label" class="step__label">{{ label }}:</strong> <slot /></div>
12
+ </template>
13
+
14
+ <style scoped>
15
+ .step__label {
16
+ font-weight: 700;
17
+ color: var(--miragon-text-primary);
18
+ }
19
+ </style>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * StepList — kompakte, beschriftete Schrittliste für die Seitenspalte neben
4
+ * einem Diagramm (Laufzeit-/Deploymentsicht). Ersetzt handgestylte
5
+ * <div style="font-size:…"><div style="margin-bottom:…"><strong style="…">-Blöcke.
6
+ *
7
+ * Styling lebt ausschließlich hier (Tokens aus theme.css), nie in slides.md.
8
+ * Verwendung:
9
+ * <StepList>
10
+ * <Step label="Client">ruft … auf</Step>
11
+ * </StepList>
12
+ */
13
+ </script>
14
+
15
+ <template>
16
+ <div class="step-list"><slot /></div>
17
+ </template>
18
+
19
+ <style scoped>
20
+ .step-list {
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 0.65rem;
24
+ font-size: 0.8rem;
25
+ line-height: 1.4;
26
+ color: var(--miragon-text-secondary);
27
+ }
28
+ </style>
@@ -0,0 +1,162 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * bpmn — Slide centered on a BPMN diagram (STATIC world, no Mesh shader).
4
+ *
5
+ * Renders a .bpmn file with the token simulation from `slidev-addon-bpmn`,
6
+ * with an optional title/eyebrow header above and an optional caption below.
7
+ * The diagram is the focal point.
8
+ *
9
+ * Requires: `slidev-addon-bpmn` must be listed in the slides.md frontmatter
10
+ * `addons:` block. The component used is `<BpmnTokenSimulation>`, registered
11
+ * automatically by the addon.
12
+ *
13
+ * Frontmatter props:
14
+ * title — slide title (h2-level)
15
+ * eyebrow — uppercase kicker
16
+ * accent — "blue" | "green" | "mixed" (default blue)
17
+ * diagram — served URL path to the .bpmn file, resolved base-aware
18
+ * (e.g. "/resources/05-diagrams/recruitment.bpmn")
19
+ * height — CSS height for the BPMN canvas (default "380px")
20
+ * Slot:
21
+ * default — optional caption / explanatory line below the diagram
22
+ */
23
+ import { computed } from 'vue'
24
+
25
+ const props = withDefaults(
26
+ defineProps<{
27
+ eyebrow?: string
28
+ accent?: 'blue' | 'green' | 'mixed'
29
+ diagram?: string
30
+ height?: string
31
+ frontmatter?: Record<string, unknown>
32
+ }>(),
33
+ { accent: 'blue', height: '380px' },
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
+ // Base-Pfad respektieren (GitHub Pages baut unter /<repo>/). Runtime-Strings
43
+ // werden von Vite NICHT umgeschrieben — daher manuell mit BASE_URL auflösen.
44
+ // Gleiche Logik wie in person.vue / content-image.vue.
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 diagramSrc = computed(() => withBase(props.diagram))
51
+ </script>
52
+
53
+ <template>
54
+ <div class="bpmn-layout" :style="{ '--bp-grad': gradientVar, '--bp-accent': accentVar }">
55
+ <div class="bpmn-inner">
56
+ <header v-if="title || eyebrow" class="bpmn-head">
57
+ <span class="bpmn-bar" aria-hidden="true"></span>
58
+ <div v-if="eyebrow" class="bpmn-eyebrow">{{ eyebrow }}</div>
59
+ <h2 v-if="title" class="bpmn-title">{{ title }}</h2>
60
+ </header>
61
+
62
+ <div class="bpmn-canvas">
63
+ <BpmnTokenSimulation
64
+ v-if="diagram"
65
+ :bpmnFilePath="diagramSrc"
66
+ width="100%"
67
+ :height="height"
68
+ />
69
+ </div>
70
+
71
+ <div v-if="$slots.default" class="bpmn-caption">
72
+ <slot />
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </template>
77
+
78
+ <style scoped>
79
+ .bpmn-layout {
80
+ position: relative;
81
+ width: 100%;
82
+ height: 100%;
83
+ overflow: hidden;
84
+ background: var(--miragon-gray-bg);
85
+ color: var(--miragon-text-primary);
86
+ display: flex;
87
+ align-items: stretch;
88
+ }
89
+
90
+ .bpmn-inner {
91
+ position: relative;
92
+ z-index: 1;
93
+ width: 100%;
94
+ max-width: 78rem;
95
+ margin: 0 auto;
96
+ padding: 2.5rem 4rem;
97
+ display: flex;
98
+ flex-direction: column;
99
+ }
100
+
101
+ .bpmn-head {
102
+ flex: 0 0 auto;
103
+ margin-bottom: 1.25rem;
104
+ }
105
+ .bpmn-bar {
106
+ display: block;
107
+ width: 3.5rem;
108
+ height: 0.35rem;
109
+ border-radius: 999px;
110
+ background: var(--bp-grad);
111
+ margin-bottom: 0.9rem;
112
+ }
113
+ .bpmn-eyebrow {
114
+ font-size: 0.9rem;
115
+ font-weight: 600;
116
+ letter-spacing: 0.18em;
117
+ text-transform: uppercase;
118
+ color: var(--bp-accent);
119
+ margin-bottom: 0.55rem;
120
+ }
121
+ .bpmn-title {
122
+ font-size: clamp(1.7rem, 2.7vw, 2.2rem);
123
+ line-height: 1.15;
124
+ font-weight: 800;
125
+ letter-spacing: -0.02em;
126
+ margin: 0;
127
+ }
128
+
129
+ .bpmn-canvas {
130
+ flex: 1 1 auto;
131
+ min-height: 0;
132
+ background: var(--miragon-white);
133
+ border: 1px solid #E5E7EB;
134
+ border-radius: 1.1rem;
135
+ padding: 0.75rem;
136
+ box-shadow: 0 8px 20px rgba(51, 93, 229, 0.08);
137
+ overflow: hidden;
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: center;
141
+ }
142
+
143
+ .bpmn-caption {
144
+ flex: 0 0 auto;
145
+ margin-top: 1rem;
146
+ font-size: 0.95rem;
147
+ color: var(--miragon-text-muted);
148
+ text-align: center;
149
+ }
150
+ .bpmn-caption :deep(p) {
151
+ margin: 0;
152
+ line-height: 1.5;
153
+ }
154
+ .bpmn-caption :deep(code) {
155
+ font-family: var(--miragon-font-mono);
156
+ font-size: 0.9em;
157
+ background: var(--miragon-blue-light);
158
+ color: var(--miragon-blue-darker);
159
+ padding: 0.1em 0.4em;
160
+ border-radius: 0.35rem;
161
+ }
162
+ </style>