@miragon/slidev-toolkit 1.5.0 → 1.6.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.
@@ -1,32 +1,23 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Agenda — the red thread at the chapter level (STATIC world, no shader).
3
+ * Agenda — a clickable chapter stepper over the live deck.
4
4
  *
5
- * A clickable stepper across the deck's chapters (numbered dots strung on one
6
- * continuous line, chapter labels centred above each dot); below it, the slides
7
- * of the selected chapter render as LIVE miniature previews (the same mechanism
8
- * the Slidev overview uses, not iframes or screenshots) and auto-fit the
9
- * remaining space, shrinking as a chapter holds more slides. Clicking a chapter
10
- * switches the preview; clicking a mini jumps to that slide. Because chapters
11
- * and slides are read from the live deck, the agenda updates itself.
5
+ * Chapters are discovered automatically: every `layout: section` slide opens a
6
+ * chapter, the slides up to the next section belong to it. Up to six chapters the
7
+ * rail is one row with each chapter's slides as live miniature previews below.
8
+ * Past six the rail WRAPS into balanced rows and drops the previews, becoming a
9
+ * static, top-aligned overview.
12
10
  *
13
- * Chapters are discovered automatically: every `layout: section` slide starts a
14
- * chapter, and the slides up to the next section belong to it.
15
- *
16
- * Frontmatter / props:
17
- * eyebrow — uppercase kicker (default "Agenda")
18
- * title — optional h2-level title
19
- * accent — "blue" | "green" | "mixed" (default mixed)
11
+ * Props: eyebrow (kicker, default "Agenda"), title (h2), accent (blue|green|mixed).
20
12
  */
21
13
  import { computed, ref, watch } from 'vue'
22
14
  import { useElementSize } from '@vueuse/core'
23
15
  import { useNav } from '@slidev/client'
24
- // The `.ts` extension is required for Vite to resolve these deep imports (the
25
- // @slidev/client `exports` map is `"./*": "./*"`, which adds no extension), but
26
- // tsc/Volar reject it without `allowImportingTsExtensions`; suppress there.
27
- // @ts-ignore - .ts extension needed for Vite resolution, not for type-checking
16
+ // The `.ts` extension is required for Vite to resolve these deep imports, but
17
+ // tsc/Volar reject it without `allowImportingTsExtensions`, so suppress there.
18
+ // @ts-ignore
28
19
  import { createFixedClicks } from '@slidev/client/composables/useClicks.ts'
29
- // @ts-ignore - .ts extension needed for Vite resolution, not for type-checking
20
+ // @ts-ignore
30
21
  import { CLICKS_MAX } from '@slidev/client/constants.ts'
31
22
  import SlideContainer from '@slidev/client/internals/SlideContainer.vue'
32
23
  import SlideWrapper from '@slidev/client/internals/SlideWrapper.vue'
@@ -59,13 +50,8 @@ interface Chapter {
59
50
  routes: any[]
60
51
  }
61
52
 
62
- // Walk the deck once: each `layout: section` slide opens a chapter, every slide
63
- // after it (until the next section) belongs to that chapter. Slides before the
64
- // first section (cover, this agenda) are intentionally ignored.
65
53
  const chapters = computed<Chapter[]>(() => {
66
54
  const out: Chapter[] = []
67
- // slides.value is typed loosely (SlideRoute's deep types don't resolve in the
68
- // editor), so annotate as any[] to read .meta/.no without TS2339.
69
55
  for (const route of slides.value as any[]) {
70
56
  const fm = route.meta?.slide?.frontmatter ?? {}
71
57
  if (fm.layout === 'section') {
@@ -85,8 +71,7 @@ const chapters = computed<Chapter[]>(() => {
85
71
 
86
72
  const selected = ref(0)
87
73
 
88
- // Keep the selection valid and follow the live position: if the deck is on a
89
- // slide that lives inside a chapter, preselect that chapter.
74
+ // Follow the live position: preselect the chapter holding the current slide.
90
75
  watch(
91
76
  [chapters, currentPage],
92
77
  ([chs, page]) => {
@@ -101,35 +86,59 @@ watch(
101
86
 
102
87
  const activeChapter = computed(() => chapters.value[selected.value])
103
88
 
104
- // A chapter of up to MAX_MINIS slides renders cleanly in the available height.
105
- // ALL slides are always rendered, but the mini size is computed as if the
106
- // chapter held at most MAX_MINIS: so the frame size never shrinks below the
107
- // clean 21-slide size, and any slides past that simply extend below the fold,
108
- // reachable by scrolling the preview.
89
+ // Mini size is computed as if a chapter held at most this many slides, so the
90
+ // frame never shrinks below the clean size; extra slides scroll below the fold.
109
91
  const MAX_MINIS = 21
110
92
 
111
- // Above five chapters the single row of labels crowds. Stagger them above/below
112
- // the line (chapter 1 above, 2 below, ...) so each label only neighbours the one
113
- // two steps away, which roughly doubles its horizontal breathing room.
93
+ // Above five chapters one label row crowds, so stagger labels above/below the line.
114
94
  const alternate = computed(() => chapters.value.length > 5)
115
95
 
116
- // Geometry of the connecting thread: dots are centred in their equal columns,
117
- // so the line runs from the first dot centre to the last, and the coloured fill
118
- // reaches the selected dot.
96
+ // Past six chapters the labels no longer fit one row: wrap the dots into balanced
97
+ // rows (max five each) and drop the previews for a static, top-aligned overview.
98
+ const WRAP_THRESHOLD = 6
99
+ const MAX_PER_ROW = 5
100
+ const wrap = computed(() => chapters.value.length > WRAP_THRESHOLD)
101
+
102
+ interface StepRow { items: Chapter[]; start: number; cols: number }
103
+ // Balanced rows sharing one column count so dots align into a grid: 8->4+4, 7->4+3.
104
+ const rows = computed<StepRow[]>(() => {
105
+ const chs = chapters.value
106
+ const rowCount = Math.max(1, Math.ceil(chs.length / MAX_PER_ROW))
107
+ const cols = Math.max(1, Math.ceil(chs.length / rowCount))
108
+ const out: StepRow[] = []
109
+ for (let i = 0; i < chs.length; i += cols) out.push({ items: chs.slice(i, i + cols), start: i, cols })
110
+ return out
111
+ })
112
+
113
+ // The chapter holding the current slide (-1 before the first chapter).
114
+ const currentIndex = computed(() =>
115
+ chapters.value.findIndex(
116
+ (c, i) =>
117
+ currentPage.value >= c.no &&
118
+ (i === chapters.value.length - 1 || currentPage.value < chapters.value[i + 1].no),
119
+ ),
120
+ )
121
+
122
+ // Per-row thread geometry: line through the row's dot centres, fill up to current.
123
+ function rowTrackLeft(row: StepRow) { return `${50 / row.cols}%` }
124
+ function rowTrackWidth(row: StepRow) { return `${((row.items.length - 1) * 100) / row.cols}%` }
125
+ function rowFillWidth(row: StepRow) {
126
+ const seg = Math.max(0, Math.min(currentIndex.value - row.start, row.items.length - 1))
127
+ return `${(seg * 100) / row.cols}%`
128
+ }
129
+
130
+ // Single-row thread geometry: line from first to last dot centre, fill to selected.
119
131
  const trackLeft = computed(() => `${50 / chapters.value.length}%`)
120
132
  const trackWidth = computed(() => `${((chapters.value.length - 1) * 100) / chapters.value.length}%`)
121
133
  const fillWidth = computed(() => `${(selected.value * 100) / chapters.value.length}%`)
122
134
 
123
- // A mini renders at fixed clicks (fully revealed). Cache one context per route.
124
135
  const clicksCtx = new WeakMap<object, ReturnType<typeof createFixedClicks>>()
125
136
  function ctxFor(route: any) {
126
137
  if (!clicksCtx.has(route)) clicksCtx.set(route, createFixedClicks(route, CLICKS_MAX))
127
138
  return clicksCtx.get(route)!
128
139
  }
129
140
 
130
- // Auto-fit the minis into whatever space is left below the stepper: measure the
131
- // stage, then pick the largest mini width whose grid (N items, 16:9) still fits
132
- // both the width and the remaining height. More slides => more rows => smaller.
141
+ // Pick the largest mini width whose grid (N items, 16:9) fits the leftover space.
133
142
  const stage = ref<HTMLElement | null>(null)
134
143
  const { width: stageW, height: stageH } = useElementSize(stage)
135
144
  const GAP = 16
@@ -137,8 +146,6 @@ const ASPECT = 16 / 9
137
146
  const MAX_W = 320
138
147
 
139
148
  const miniWidth = computed(() => {
140
- // Size as if the chapter held at most MAX_MINIS slides, so the frame never
141
- // shrinks below the clean 21-slide size; extra slides scroll below the fold.
142
149
  const n = Math.min(activeChapter.value?.routes.length ?? 0, MAX_MINIS)
143
150
  const W = stageW.value
144
151
  const H = stageH.value
@@ -150,13 +157,11 @@ const miniWidth = computed(() => {
150
157
  const totalH = rows * (w / ASPECT) + (rows - 1) * GAP
151
158
  if (totalH <= H && w > best) best = w
152
159
  }
153
- if (!best) best = (W - (n - 1) * GAP) / n // never larger than one row across
160
+ if (!best) best = (W - (n - 1) * GAP) / n
154
161
  return Math.max(96, Math.min(best, MAX_W))
155
162
  })
156
163
 
157
- // After a click, drop focus off the <button> back to the document, otherwise the
158
- // focused button swallows Slidev's arrow-key navigation and the next-slide key
159
- // stops working until the user clicks the slide again.
164
+ // Blur after a click so the button stops swallowing Slidev's arrow-key navigation.
160
165
  function selectChapter(i: number, ev: MouseEvent) {
161
166
  selected.value = i
162
167
  ;(ev.currentTarget as HTMLElement | null)?.blur()
@@ -176,8 +181,30 @@ function openSlide(no: number, ev: MouseEvent) {
176
181
  <h2 v-if="title" class="agenda-title">{{ title }}</h2>
177
182
  </header>
178
183
 
179
- <!-- Stepper: labels centred above numbered dots strung on one line. -->
180
- <nav class="agenda-stepper" :class="{ 'is-alternating': alternate }" aria-label="Chapters">
184
+ <!-- Many chapters: wrapped rows, top-aligned, static (no previews to navigate). -->
185
+ <nav v-if="wrap" class="agenda-stepper is-wrap" aria-label="Chapters">
186
+ <div v-for="(row, r) in rows" :key="r" class="stepper-row" :style="{ '--cols': row.cols }">
187
+ <div class="track" aria-hidden="true">
188
+ <span class="track-line" :style="{ left: rowTrackLeft(row), width: rowTrackWidth(row) }"></span>
189
+ <span class="track-fill" :style="{ left: rowTrackLeft(row), width: rowFillWidth(row) }"></span>
190
+ </div>
191
+ <div
192
+ v-for="(ch, i) in row.items"
193
+ :key="ch.no"
194
+ class="step is-static"
195
+ :class="{ 'is-active': row.start + i === currentIndex, 'is-done': row.start + i < currentIndex }"
196
+ >
197
+ <span class="step-meta">
198
+ <span class="step-eyebrow">{{ ch.eyebrow }}</span>
199
+ <span class="step-label">{{ ch.title }}</span>
200
+ </span>
201
+ <span class="step-dot"><span class="step-num">{{ row.start + i + 1 }}</span></span>
202
+ </div>
203
+ </div>
204
+ </nav>
205
+
206
+ <!-- Up to six chapters: one clickable row that drives the previews below. -->
207
+ <nav v-else class="agenda-stepper" :class="{ 'is-alternating': alternate }" aria-label="Chapters">
181
208
  <div class="track" aria-hidden="true">
182
209
  <span class="track-line" :style="{ left: trackLeft, width: trackWidth }"></span>
183
210
  <span class="track-fill" :style="{ left: trackLeft, width: fillWidth }"></span>
@@ -198,8 +225,7 @@ function openSlide(no: number, ev: MouseEvent) {
198
225
  </button>
199
226
  </nav>
200
227
 
201
- <!-- Preview: the selected chapter's slides as live miniatures, auto-fit. -->
202
- <div class="agenda-preview">
228
+ <div v-if="!wrap" class="agenda-preview">
203
229
  <div ref="stage" class="preview-stage">
204
230
  <transition name="fade-preview" mode="out-in">
205
231
  <div :key="selected" class="preview-row">
@@ -227,8 +253,7 @@ function openSlide(no: number, ev: MouseEvent) {
227
253
 
228
254
  <style scoped>
229
255
  .agenda-layout {
230
- /* Declared so the custom properties resolve statically (linter + fallback);
231
- the inline :style binding on this element overrides them at runtime. */
256
+ /* Fallback for the linter; the inline :style binding overrides at runtime. */
232
257
  --ag-grad: var(--miragon-gradient-mixed);
233
258
  --ag-accent: var(--miragon-blue);
234
259
  position: absolute;
@@ -290,7 +315,6 @@ function openSlide(no: number, ev: MouseEvent) {
290
315
  align-items: end;
291
316
  margin-bottom: 1.5rem;
292
317
  }
293
- /* the one continuous thread, drawn through the dot centres */
294
318
  .track {
295
319
  position: absolute;
296
320
  left: 0;
@@ -374,9 +398,7 @@ function openSlide(no: number, ev: MouseEvent) {
374
398
  }
375
399
  .step.is-done .step-dot { border-color: var(--ag-accent); }
376
400
  .step.is-done .step-num { color: var(--ag-accent); }
377
- /* Solid accent fill, not the mixed gradient: on a small circle the blue->green
378
- gradient muddies into an ugly green corner. Flat fill, border matches the fill
379
- so no inner ring (no inset sheen) shows through. */
401
+ /* Flat accent fill (a gradient muddies into green on a small circle). */
380
402
  .step.is-active .step-dot {
381
403
  background: var(--ag-accent);
382
404
  border-color: var(--ag-accent);
@@ -385,20 +407,13 @@ function openSlide(no: number, ev: MouseEvent) {
385
407
  }
386
408
  .step.is-active .step-num { color: var(--miragon-white); }
387
409
 
388
- /* ---- Alternating labels (only above five chapters) ---------------------- */
389
- /* One label row crowds past five chapters, so stagger labels above/below the
390
- line. Each step becomes a grid with FIXED, EQUAL top and bottom zones and the
391
- dot in the centre row, so every dot lands dead centre on the one line no
392
- matter which side carries the label. Labels are single-line (nowrap), so a
393
- fixed zone always fits eyebrow + title. */
410
+ /* ---- Alternating labels (above five chapters) --------------------------- */
411
+ /* Dot stays in flow so columns stay equal; each label is absolute and centred on
412
+ its dot, above for odd chapters and below for even, so a wide label can't widen
413
+ its column. Labels are single-line, so the fixed zone always fits them. */
394
414
  .agenda-stepper.is-alternating {
395
415
  align-items: stretch;
396
416
  }
397
- /* Only the dot stays in flow (a fixed 2.2rem, far under a 1fr column), so the
398
- columns remain strictly equal and the dots are evenly spaced. Each label is
399
- taken OUT of flow (absolute) and centred on its own dot, above for odd
400
- chapters and below for even ones, so a wide label can never widen its column
401
- and throw the spacing off. */
402
417
  .agenda-stepper.is-alternating .step {
403
418
  position: relative;
404
419
  min-width: 0;
@@ -419,23 +434,55 @@ function openSlide(no: number, ev: MouseEvent) {
419
434
  .agenda-stepper.is-alternating .step.label-below .step-meta {
420
435
  top: calc(50% + 1.7rem);
421
436
  bottom: auto;
422
- /* Flip eyebrow/title so the title sits next to the line and "Kapitel 0X" stays
423
- on the outer edge, mirroring the row above. */
424
437
  flex-direction: column-reverse;
425
438
  }
426
- /* Labels are centred on their dot and same-level neighbours are two steps apart,
427
- so show them at full width instead of clipping to one column. */
428
439
  .agenda-stepper.is-alternating .step-label {
429
440
  overflow: visible;
430
441
  text-overflow: clip;
431
442
  max-width: none;
432
443
  }
433
- /* The thread runs through the vertically centred dots. */
434
444
  .agenda-stepper.is-alternating .track {
435
445
  top: 50%;
436
446
  bottom: auto;
437
447
  }
438
448
 
449
+ /* ---- Wrapping rail (above six chapters, no previews) -------------------- */
450
+ /* Each row is its own grid with its own thread, so the line never bridges a wrap.
451
+ Top-aligned to hug the head instead of floating in whitespace. */
452
+ .agenda-stepper.is-wrap {
453
+ display: flex;
454
+ flex-direction: column;
455
+ align-items: stretch;
456
+ justify-content: flex-start;
457
+ gap: 2.8rem;
458
+ flex: 1 1 auto;
459
+ min-height: 0;
460
+ margin-bottom: 0;
461
+ }
462
+ .is-wrap .step.is-static { cursor: default; }
463
+ .stepper-row {
464
+ position: relative;
465
+ display: grid;
466
+ grid-template-columns: repeat(var(--cols), 1fr);
467
+ align-items: end;
468
+ }
469
+ /* Reserved height so two-line labels bottom-align and every dot lands on the line. */
470
+ .is-wrap .step-meta {
471
+ min-height: 3.4rem;
472
+ justify-content: flex-end;
473
+ }
474
+ .is-wrap .step-label {
475
+ white-space: normal;
476
+ display: -webkit-box;
477
+ -webkit-line-clamp: 2;
478
+ line-clamp: 2;
479
+ -webkit-box-orient: vertical;
480
+ overflow: hidden;
481
+ text-overflow: ellipsis;
482
+ max-width: 100%;
483
+ line-height: 1.2;
484
+ }
485
+
439
486
  /* ---- Preview ------------------------------------------------------------ */
440
487
  .agenda-preview {
441
488
  flex: 1 1 auto;
@@ -446,18 +493,13 @@ function openSlide(no: number, ev: MouseEvent) {
446
493
  .preview-stage {
447
494
  flex: 1 1 auto;
448
495
  min-height: 0;
449
- /* Safety net: minis are sized to fit up to MAX_MINIS within this height, but
450
- if a capped chapter still overflows (short viewport), scroll to reach the
451
- rest rather than clip or shrink them further. */
452
- overflow-y: auto;
496
+ overflow-y: auto; /* scroll to reach the rest if a capped chapter overflows */
453
497
  }
454
498
  .preview-row {
455
499
  min-height: 100%;
456
500
  display: flex;
457
501
  flex-wrap: wrap;
458
- /* `safe` centers when the minis fit, but falls back to top-alignment once the
459
- chapter overflows, so the first row never lands above the scroll origin
460
- (where it would be unreachable). */
502
+ /* `safe` centers when minis fit, else top-aligns so the first row stays reachable. */
461
503
  align-content: safe center;
462
504
  justify-content: center;
463
505
  gap: 16px;
@@ -90,17 +90,37 @@ const ACCENTS: Record<string, string> = {
90
90
  line-height: 1.55;
91
91
  color: #4b5563;
92
92
  }
93
- /* Wrapping the body in blank lines (required for inline Markdown `code`, **bold**,
94
- links — to render inside the slot; see the slides skill) makes Markdown emit a
95
- <p>. Left alone it would inherit Slidev's larger prose paragraph size and default
96
- margins, so a block-form card would look different from a tight one. Normalise it:
97
- inherit the card body size, no outer margins, even spacing between paragraphs. */
93
+ /* Blank-line-wrapped inline Markdown emits a <p>; normalise it to the card body size (no prose bloat). */
98
94
  .mg-card__body :deep(p) {
99
95
  margin: 0;
100
96
  font-size: inherit;
101
97
  line-height: inherit;
102
98
  }
103
99
  .mg-card__body :deep(p + p) { margin-top: 0.6em; }
100
+ /* Same for bullet lists: normalise to the body size with a small blue square marker (like the content layout). */
101
+ .mg-card__body :deep(ul) {
102
+ list-style: none;
103
+ margin: 0;
104
+ padding: 0;
105
+ }
106
+ .mg-card__body :deep(li) {
107
+ position: relative;
108
+ font-size: inherit;
109
+ line-height: inherit;
110
+ padding-left: 1.1rem;
111
+ margin: 0;
112
+ }
113
+ .mg-card__body :deep(li + li) { margin-top: 0.4em; }
114
+ .mg-card__body :deep(li)::before {
115
+ content: '';
116
+ position: absolute;
117
+ left: 0;
118
+ top: 0.55em;
119
+ width: 0.4rem;
120
+ height: 0.4rem;
121
+ border-radius: 0.1rem;
122
+ background: var(--miragon-blue);
123
+ }
104
124
  .mg-card--compact .mg-card__body { flex: 1 1 auto; display: flex; flex-direction: column; }
105
125
  /* A trailing diagram (<Figure src>) docks at the card bottom, so the image sits at
106
126
  the same position in every card of a stretched grid regardless of text length. */
@@ -1,6 +1,8 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Step — eine Zeile in einer <StepList>: fett gesetztes Label + Fließtext.
3
+ * Step — eine Zeile in einer <StepList>: nummeriertes Badge + fett gesetztes
4
+ * Label + Fließtext. Die Nummer kommt aus einem CSS-Counter, den die StepList
5
+ * zurücksetzt und jeder Step erhöht (kein index-Prop nötig).
4
6
  * Das Label nutzt die primäre Textfarbe, der Body erbt die Farbe der StepList.
5
7
  * <Step label="Client">ruft … auf</Step>
6
8
  */
@@ -8,10 +10,35 @@ defineProps<{ label?: string }>()
8
10
  </script>
9
11
 
10
12
  <template>
11
- <div class="step"><strong v-if="label" class="step__label">{{ label }}:</strong> <slot /></div>
13
+ <div class="step">
14
+ <span class="step__marker" aria-hidden="true"></span>
15
+ <span class="step__body"><strong v-if="label" class="step__label">{{ label }}:</strong> <slot /></span>
16
+ </div>
12
17
  </template>
13
18
 
14
19
  <style scoped>
20
+ .step {
21
+ display: flex;
22
+ align-items: baseline;
23
+ gap: 0.6rem;
24
+ counter-increment: miragon-step;
25
+ }
26
+ .step__marker {
27
+ flex: 0 0 auto;
28
+ align-self: flex-start;
29
+ width: 1.5rem;
30
+ height: 1.5rem;
31
+ border-radius: 50%;
32
+ background: var(--miragon-blue);
33
+ color: #fff;
34
+ font-size: 0.75rem;
35
+ font-weight: 700;
36
+ line-height: 1.5rem;
37
+ text-align: center;
38
+ }
39
+ .step__marker::before {
40
+ content: counter(miragon-step);
41
+ }
15
42
  .step__label {
16
43
  font-weight: 700;
17
44
  color: var(--miragon-text-primary);
@@ -20,9 +20,10 @@
20
20
  .step-list {
21
21
  display: flex;
22
22
  flex-direction: column;
23
- gap: 0.65rem;
24
- font-size: 0.8rem;
25
- line-height: 1.4;
23
+ gap: 0.95rem;
24
+ font-size: 1.2rem;
25
+ line-height: 1.5;
26
26
  color: var(--miragon-text-secondary);
27
+ counter-reset: miragon-step;
27
28
  }
28
29
  </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miragon/slidev-toolkit",
3
- "version": "1.5.0",
3
+ "version": "1.6.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",
@@ -20,7 +20,10 @@
20
20
  "slidev": ">=0.48.0"
21
21
  },
22
22
  "dependencies": {
23
- "@paper-design/shaders": "0.0.77"
23
+ "@paper-design/shaders": "0.0.78"
24
+ },
25
+ "devDependencies": {
26
+ "@slidev/types": "52.18.0"
24
27
  },
25
28
  "peerDependencies": {
26
29
  "vue": "^3.5.0"
@@ -31,6 +34,7 @@
31
34
  "styles",
32
35
  "assets",
33
36
  "global",
37
+ "setup",
34
38
  "global-top.vue",
35
39
  "global-bottom.vue"
36
40
  ],
@@ -0,0 +1,32 @@
1
+ import { defineMermaidSetup } from '@slidev/types'
2
+
3
+ // Brand-styles every ```mermaid diagram with the Miragon palette and Geist, so
4
+ // text-generated diagrams stay on-brand. Slidev collects setup/* from every root
5
+ // (theme included), so shipping this in the theme package means every deck that
6
+ // sets `theme: '@miragon/slidev-toolkit'` inherits it with zero config. A deck can
7
+ // still override by adding its own setup/mermaid.ts, but never has to.
8
+ // The hex values mirror the tokens in ../styles/theme.css; they live here (config),
9
+ // never in slide markdown, so the "no hardcoded hex in markdown" rule stays intact.
10
+ // Excalidraw remains the default for diagrams; Mermaid is for text-generated flows.
11
+ export default defineMermaidSetup(() => ({
12
+ theme: 'base',
13
+ themeVariables: {
14
+ // Nodes: light-blue fill, Miragon-blue border, black label text
15
+ primaryColor: '#F0F4FF', // --miragon-blue-light
16
+ primaryBorderColor: '#335DE5', // --miragon-blue
17
+ primaryTextColor: '#000000', // --miragon-text-primary
18
+
19
+ // Edges and their labels
20
+ lineColor: '#335DE5', // --miragon-blue
21
+ edgeLabelBackground: '#F5F7FF', // --miragon-blue-soft
22
+
23
+ // Secondary / tertiary surfaces (sequence actor boxes, alt-blocks, notes)
24
+ secondaryColor: '#F5F7FF', // --miragon-blue-soft
25
+ secondaryBorderColor: '#2B4ACB', // --miragon-blue-mid
26
+ tertiaryColor: '#FFFFFF',
27
+ tertiaryBorderColor: '#335DE5',
28
+
29
+ // Typography: Geist, matching the theme's body font
30
+ fontFamily: "'Geist', 'Inter', 'Helvetica Neue', Arial, sans-serif",
31
+ },
32
+ }))