@miragon/slidev-toolkit 1.0.0 → 1.2.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,144 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ChapterFooter — dezenter Footer: Kapitel + Fortschritt, optional Datum links
4
+ * (globales Chrome).
5
+ *
6
+ * Implementierung, die vom globalen Layer `global-bottom.vue` (Theme-Root) auf
7
+ * jeder Slide gerendert wird. Liegt bewusst in `global/` (nicht `components/`):
8
+ * `components/` sind autoren-aufrufbare Tags, dies ist Chrome, das Slidev
9
+ * automatisch injiziert.
10
+ *
11
+ * Zeigt ein kleines Label unten links, z. B.:
12
+ * "21 July 2026 · 01 · Welcome · 2 / 5"
13
+ *
14
+ * Kapitel-Erkennung (identisch zur Agenda.vue): jede `layout: section`-Slide
15
+ * öffnet ein Kapitel; die Slides bis zur nächsten section gehören dazu. Die
16
+ * Position zählt nur Content-Slides — die section-Slide selbst ist Position 0,
17
+ * sodass die erste Content-Slide "1 / N" statt "2 / N" liest.
18
+ *
19
+ * Regeln:
20
+ * - Rein visuelles Overlay: transparenter Hintergrund, `pointer-events: none`,
21
+ * verdeckt niemals Slide-Inhalt (malt nur das kleine Text-Label).
22
+ * - Ausgeblendet auf cover/closing/section und überall, wo (noch) kein
23
+ * Kapitel aktiv ist (Cover, Agenda vor der ersten section).
24
+ * - Aktuelles Layout MUSS über useNav() gelesen werden — `$frontmatter` aus
25
+ * useSlideContext() ist in globalen Komponenten NICHT reaktiv.
26
+ */
27
+ import { computed } from 'vue'
28
+ import { useNav } from '@slidev/client'
29
+
30
+ const { currentPage, total, slides } = useNav()
31
+
32
+ // Markdown-Bold (** **) überlebt in den auto-extrahierten Titel; entfernen.
33
+ function clean(s?: string): string {
34
+ return (s ?? '').replace(/\*\*/g, '').trim()
35
+ }
36
+
37
+ interface Chapter {
38
+ no: number // 1-basierte Seitennummer der section-Slide
39
+ index: string // Kapitelnummer (aus frontmatter `index`, sonst Ordinalzahl)
40
+ title: string // Kapiteltitel (h1 der section-Slide)
41
+ }
42
+
43
+ // Deck einmal durchlaufen: jede section-Slide startet ein Kapitel.
44
+ const chapters = computed<Chapter[]>(() => {
45
+ const out: Chapter[] = []
46
+ for (const route of slides.value as any[]) {
47
+ const fm = route.meta?.slide?.frontmatter ?? {}
48
+ if (fm.layout === 'section') {
49
+ const n = out.length + 1
50
+ out.push({
51
+ no: route.no,
52
+ index: clean(fm.index) || String(n).padStart(2, '0'),
53
+ title:
54
+ clean(route.meta?.slide?.title) || clean(fm.eyebrow) || `Chapter ${n}`,
55
+ })
56
+ }
57
+ }
58
+ return out
59
+ })
60
+
61
+ // Das Kapitel, in dem die aktuelle Slide liegt (mit Startseite der nächsten
62
+ // section als obere Grenze; für das letzte Kapitel ist die Grenze total + 1).
63
+ const current = computed(() => {
64
+ const chs = chapters.value
65
+ const page = currentPage.value
66
+ for (let i = 0; i < chs.length; i++) {
67
+ const next = i < chs.length - 1 ? chs[i + 1].no : total.value + 1
68
+ if (page >= chs[i].no && page < next) {
69
+ return { ...chs[i], next }
70
+ }
71
+ }
72
+ return null
73
+ })
74
+
75
+ // "position / total" innerhalb des Kapitels; section-Slide selbst zählt nicht.
76
+ const chapterProgress = computed(() => {
77
+ const c = current.value
78
+ if (!c) return ''
79
+ const position = currentPage.value - c.no
80
+ const totalInChapter = c.next - 1 - c.no
81
+ if (position < 1 || totalInChapter < 1) return ''
82
+ return `${position} / ${totalInChapter}`
83
+ })
84
+
85
+ // Optionales Datum: Feld `eventDate` im Deck-Headmatter (erster Frontmatter-
86
+ // Block = Frontmatter der ersten Slide). Fehlt es, bleibt das Segment leer.
87
+ const dateLabel = computed(() => {
88
+ const raw = (slides.value?.[0] as any)?.meta?.slide?.frontmatter?.eventDate
89
+ if (!raw) return ''
90
+ const d = new Date(raw)
91
+ if (isNaN(d.getTime())) return ''
92
+ return new Intl.DateTimeFormat('en-GB', {
93
+ day: 'numeric',
94
+ month: 'long',
95
+ year: 'numeric',
96
+ }).format(d)
97
+ })
98
+
99
+ // Segmente in gewünschter Reihenfolge zusammensetzen: Datum · NN · Titel · n/N.
100
+ const footerText = computed(() => {
101
+ const c = current.value
102
+ if (!c) return ''
103
+ const parts: string[] = []
104
+ if (dateLabel.value) parts.push(dateLabel.value)
105
+ if (c.index) parts.push(c.index)
106
+ if (c.title) parts.push(c.title)
107
+ if (chapterProgress.value) parts.push(chapterProgress.value)
108
+ return parts.join(' · ')
109
+ })
110
+
111
+ // Auf den Voll-Bild-Slides ausblenden (gleiche Menge wie ProgressBar.vue).
112
+ const HIDDEN_LAYOUTS = ['cover', 'closing', 'section']
113
+ const isHidden = computed(() => {
114
+ const layout = (slides.value?.[currentPage.value - 1] as any)?.meta?.slide
115
+ ?.frontmatter?.layout
116
+ return HIDDEN_LAYOUTS.includes(layout)
117
+ })
118
+ </script>
119
+
120
+ <template>
121
+ <div v-if="!isHidden && footerText" class="miragon-footer" aria-hidden="true">
122
+ <span class="miragon-footer-text">{{ footerText }}</span>
123
+ </div>
124
+ </template>
125
+
126
+ <style scoped>
127
+ .miragon-footer {
128
+ position: fixed;
129
+ bottom: 0;
130
+ left: 0;
131
+ z-index: 40;
132
+ display: flex;
133
+ align-items: center;
134
+ padding: 0 2rem 0.75rem;
135
+ background: transparent;
136
+ pointer-events: none;
137
+ }
138
+ .miragon-footer-text {
139
+ font-size: 0.6rem;
140
+ font-weight: 600;
141
+ letter-spacing: 0.03em;
142
+ color: var(--miragon-text-muted);
143
+ }
144
+ </style>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ProgressBar — dünne Fortschritts-Stepper-Bar (globales Chrome).
4
+ *
5
+ * Implementierung, die vom globalen Layer `global-top.vue` (Theme-Root) auf
6
+ * jeder Slide gerendert wird. Liegt bewusst in `global/` (nicht `components/`):
7
+ * `components/` sind autoren-aufrufbare Tags (<Card> …), dies ist Chrome, das
8
+ * Slidev automatisch injiziert.
9
+ *
10
+ * Rendert einen schmalen Gradient-Balken am oberen Rand, dessen Breite dem
11
+ * Deck-Fortschritt (currentPage / total) entspricht.
12
+ *
13
+ * Regeln:
14
+ * - Rein visuelles Overlay: transparenter Rest, `pointer-events: none`,
15
+ * verdeckt niemals Slide-Inhalt (nur der Verlauf malt Pixel).
16
+ * - Ausgeblendet auf den animierten Brand-Momenten (cover/closing) und dem
17
+ * Kapiteltrenner (section), damit diese Voll-Bild-Slides ruhig bleiben.
18
+ * - Das aktuelle Layout MUSS über useNav() gelesen werden — `$frontmatter`
19
+ * aus useSlideContext() ist in globalen Komponenten NICHT reaktiv und
20
+ * liefert veraltete Werte.
21
+ */
22
+ import { computed } from 'vue'
23
+ import { useNav } from '@slidev/client'
24
+
25
+ const { currentPage, total, slides } = useNav()
26
+
27
+ // Deck-Fortschritt in Prozent (0..100).
28
+ const progress = computed(() =>
29
+ total.value ? (currentPage.value / total.value) * 100 : 0,
30
+ )
31
+
32
+ // Layouts, auf denen die Bar ausgeblendet wird (siehe ChapterFooter.vue —
33
+ // bewusst dieselbe Menge, damit Chrome konsistent verschwindet).
34
+ const HIDDEN_LAYOUTS = ['cover', 'closing', 'section']
35
+ const isHidden = computed(() => {
36
+ const layout = (slides.value?.[currentPage.value - 1] as any)?.meta?.slide
37
+ ?.frontmatter?.layout
38
+ return HIDDEN_LAYOUTS.includes(layout)
39
+ })
40
+ </script>
41
+
42
+ <template>
43
+ <div
44
+ v-if="!isHidden"
45
+ class="miragon-progress"
46
+ :style="{ width: progress + '%' }"
47
+ aria-hidden="true"
48
+ ></div>
49
+ </template>
50
+
51
+ <style scoped>
52
+ .miragon-progress {
53
+ position: fixed;
54
+ top: 0;
55
+ left: 0;
56
+ height: 4px;
57
+ z-index: 50;
58
+ /* Verlauf aus Brand-Tokens (Blau -> Grün), keine hardcoded Hex. */
59
+ background: linear-gradient(
60
+ 90deg,
61
+ var(--miragon-blue) 0%,
62
+ var(--miragon-green) 100%
63
+ );
64
+ border-top-right-radius: 2px;
65
+ border-bottom-right-radius: 2px;
66
+ pointer-events: none;
67
+ transition: width 0.3s ease;
68
+ }
69
+ </style>
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * global-bottom — Slidev-Einhängepunkt für den unteren globalen Layer.
4
+ *
5
+ * Slidev injiziert per Namenskonvention NUR Dateien namens `global-bottom.*`
6
+ * aus dem Theme-Root (Unterordner werden nicht gescannt). Diese Datei ist daher
7
+ * ein schlanker Shim; die eigentliche Logik liegt aufgeräumt in `global/`.
8
+ */
9
+ import ChapterFooter from './global/ChapterFooter.vue'
10
+ </script>
11
+
12
+ <template>
13
+ <ChapterFooter></ChapterFooter>
14
+ </template>
package/global-top.vue ADDED
@@ -0,0 +1,14 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * global-top — Slidev-Einhängepunkt für den oberen globalen Layer.
4
+ *
5
+ * Slidev injiziert per Namenskonvention NUR Dateien namens `global-top.*` aus
6
+ * dem Theme-Root (Unterordner werden nicht gescannt). Diese Datei ist daher ein
7
+ * schlanker Shim; die eigentliche Logik liegt aufgeräumt in `global/`.
8
+ */
9
+ import ProgressBar from './global/ProgressBar.vue'
10
+ </script>
11
+
12
+ <template>
13
+ <ProgressBar></ProgressBar>
14
+ </template>
@@ -0,0 +1,176 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * dmn — Slide centered on a DMN decision table (STATIC world, no Mesh shader).
4
+ *
5
+ * Renders a .dmn file as a decision table via `slidev-addon-dmn`, with an
6
+ * optional title/eyebrow header above and an optional caption below. The
7
+ * decision table is the focal point. The sibling of the `bpmn` archetype: BPMN
8
+ * models the process, DMN models the decisions inside it.
9
+ *
10
+ * Requires: `slidev-addon-dmn` must be listed in the slides.md frontmatter
11
+ * `addons:` block. The component used is `<DmnTable>`, registered automatically
12
+ * by the addon (it also ships `<DmnDrd>` for the requirement diagram and
13
+ * `<DmnModeler>` for an editor, not used here).
14
+ *
15
+ * Frontmatter props:
16
+ * title — slide title (h2-level)
17
+ * eyebrow — uppercase kicker
18
+ * accent — "blue" | "green" | "mixed" (default blue)
19
+ * diagram — served URL path to the .dmn file, resolved base-aware
20
+ * (e.g. "/resources/04-diagrams/approval.dmn")
21
+ * height — CSS height for the table canvas (default "360px")
22
+ * decisionId — which decision to show when the file holds several (optional)
23
+ * fontSize — table font size (default "15px")
24
+ * showAnnotations — show the trailing annotations column (default false)
25
+ * Slot:
26
+ * default — optional caption / explanatory line below the table
27
+ */
28
+ import { computed } from 'vue'
29
+
30
+ const props = withDefaults(
31
+ defineProps<{
32
+ eyebrow?: string
33
+ accent?: 'blue' | 'green' | 'mixed'
34
+ diagram?: string
35
+ height?: string
36
+ decisionId?: string
37
+ fontSize?: string
38
+ showAnnotations?: boolean
39
+ frontmatter?: Record<string, unknown>
40
+ }>(),
41
+ { accent: 'blue', height: '360px', fontSize: '15px', showAnnotations: false },
42
+ )
43
+
44
+ const title = computed(() => props.frontmatter?.title as string | undefined)
45
+ const gradientVar = computed(() => `var(--miragon-gradient-${props.accent})`)
46
+ const accentVar = computed(() =>
47
+ props.accent === 'green' ? 'var(--miragon-green-deep)' : 'var(--miragon-blue)',
48
+ )
49
+
50
+ // Base-Pfad respektieren (Deploys bauen ggf. unter /<repo>/). Runtime-Strings
51
+ // werden von Vite NICHT umgeschrieben — daher manuell mit BASE_URL auflösen.
52
+ // Gleiche Logik wie in bpmn.vue / person.vue / content-image.vue.
53
+ function withBase(path?: string) {
54
+ if (!path) return path
55
+ if (/^https?:\/\//.test(path)) return path
56
+ return import.meta.env.BASE_URL.replace(/\/$/, '') + '/' + path.replace(/^\//, '')
57
+ }
58
+ const diagramSrc = computed(() => withBase(props.diagram))
59
+ </script>
60
+
61
+ <template>
62
+ <div class="dmn-layout" :style="{ '--dm-grad': gradientVar, '--dm-accent': accentVar }">
63
+ <div class="dmn-inner">
64
+ <header v-if="title || eyebrow" class="dmn-head">
65
+ <span class="dmn-bar" aria-hidden="true"></span>
66
+ <div v-if="eyebrow" class="dmn-eyebrow">{{ eyebrow }}</div>
67
+ <h2 v-if="title" class="dmn-title">{{ title }}</h2>
68
+ </header>
69
+
70
+ <div class="dmn-canvas">
71
+ <DmnTable
72
+ v-if="diagram"
73
+ :dmnFilePath="diagramSrc"
74
+ width="100%"
75
+ :height="height"
76
+ :decisionId="decisionId"
77
+ :fontSize="fontSize"
78
+ :showAnnotations="showAnnotations"
79
+ />
80
+ </div>
81
+
82
+ <div v-if="$slots.default" class="dmn-caption">
83
+ <slot />
84
+ </div>
85
+ </div>
86
+ </div>
87
+ </template>
88
+
89
+ <style scoped>
90
+ .dmn-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
+ .dmn-inner {
102
+ position: relative;
103
+ z-index: 1;
104
+ width: 100%;
105
+ max-width: 78rem;
106
+ margin: 0 auto;
107
+ padding: 2.5rem 4rem;
108
+ display: flex;
109
+ flex-direction: column;
110
+ }
111
+
112
+ .dmn-head {
113
+ flex: 0 0 auto;
114
+ margin-bottom: 1.25rem;
115
+ }
116
+ .dmn-bar {
117
+ display: block;
118
+ width: 3.5rem;
119
+ height: 0.35rem;
120
+ border-radius: 999px;
121
+ background: var(--dm-grad);
122
+ margin-bottom: 0.9rem;
123
+ }
124
+ .dmn-eyebrow {
125
+ font-size: 0.9rem;
126
+ font-weight: 600;
127
+ letter-spacing: 0.18em;
128
+ text-transform: uppercase;
129
+ color: var(--dm-accent);
130
+ margin-bottom: 0.55rem;
131
+ }
132
+ .dmn-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
+ .dmn-canvas {
141
+ flex: 1 1 auto;
142
+ min-height: 0;
143
+ background: var(--miragon-white);
144
+ border: 1px solid #E5E7EB;
145
+ border-radius: 1.1rem;
146
+ padding: 0.75rem;
147
+ box-shadow: 0 8px 20px rgba(51, 93, 229, 0.08);
148
+ overflow: hidden;
149
+ display: flex;
150
+ align-items: center;
151
+ justify-content: center;
152
+ }
153
+ /* dmn-js ships its own decision-table CSS (imported by the addon). We keep that
154
+ rendering intact and only frame it in the branded card above, exactly like the
155
+ bpmn archetype frames bpmn-js. */
156
+
157
+ .dmn-caption {
158
+ flex: 0 0 auto;
159
+ margin-top: 1rem;
160
+ font-size: 0.95rem;
161
+ color: var(--miragon-text-muted);
162
+ text-align: center;
163
+ }
164
+ .dmn-caption :deep(p) {
165
+ margin: 0;
166
+ line-height: 1.5;
167
+ }
168
+ .dmn-caption :deep(code) {
169
+ font-family: var(--miragon-font-mono);
170
+ font-size: 0.9em;
171
+ background: var(--miragon-blue-light);
172
+ color: var(--miragon-blue-darker);
173
+ padding: 0.1em 0.4em;
174
+ border-radius: 0.35rem;
175
+ }
176
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miragon/slidev-toolkit",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
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
5
  "keywords": [
6
6
  "slidev-theme",
@@ -23,13 +23,16 @@
23
23
  "@paper-design/shaders": "0.0.77"
24
24
  },
25
25
  "peerDependencies": {
26
- "vue": "^3.5"
26
+ "vue": "3.5.40"
27
27
  },
28
28
  "files": [
29
29
  "layouts",
30
30
  "components",
31
31
  "styles",
32
- "assets"
32
+ "assets",
33
+ "global",
34
+ "global-top.vue",
35
+ "global-bottom.vue"
33
36
  ],
34
37
  "publishConfig": {
35
38
  "access": "public",