@miragon/slidev-toolkit 1.0.0 → 1.1.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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miragon/slidev-toolkit",
3
- "version": "1.0.0",
3
+ "version": "1.1.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",