@miragon/slidev-toolkit 1.5.1 → 1.7.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/README.md +1 -1
- package/components/Agenda.vue +123 -81
- package/components/DiagramFrame.vue +36 -0
- package/layouts/bpmn.vue +4 -11
- package/layouts/dmn.vue +6 -13
- package/layouts/excalidraw.vue +131 -0
- package/layouts/mermaid.vue +130 -0
- package/package.json +6 -2
- package/setup/mermaid.ts +25 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The Miragon-branded [Slidev](https://sli.dev) design system: a fixed brand theme, twelve layout archetypes, reusable components (`Card`, `CardGrid`, `StepList`, `Figure`, `SplitView`, `Agenda`), the animated `BrandMeshBackground`, and always-on progress chrome (a stepper bar and a chapter footer). One source of design truth, so a Miragon deck stays on-brand without hand-written HTML or CSS.
|
|
4
4
|
|
|
5
|
-
> Building a **new** deck? The easiest path is the [template repository](https://github.com/Miragon/slidev-deck-template)
|
|
5
|
+
> Building a **new** deck? The easiest path is `npm create @miragon/slidev-deck@latest my-talk` (from the [template repository](https://github.com/Miragon/slidev-deck-template)) — it scaffolds this toolkit together with a demo deck, authoring guidance, and CI. Use the npm package below when you want the brand in an **existing** or standalone Slidev project.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
package/components/Agenda.vue
CHANGED
|
@@ -1,32 +1,23 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Agenda —
|
|
3
|
+
* Agenda — a clickable chapter stepper over the live deck.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
*
|
|
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
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
105
|
-
//
|
|
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
|
|
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
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
<!--
|
|
180
|
-
<nav
|
|
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
|
-
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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 (
|
|
389
|
-
/*
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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
|
|
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;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
withDefaults(
|
|
3
|
+
defineProps<{
|
|
4
|
+
padding?: 'compact' | 'standard' | 'generous'
|
|
5
|
+
height?: string
|
|
6
|
+
}>(),
|
|
7
|
+
{ padding: 'standard' },
|
|
8
|
+
)
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<div
|
|
13
|
+
class="mg-frame"
|
|
14
|
+
:class="`mg-frame--${padding}`"
|
|
15
|
+
:style="height ? { height } : undefined"
|
|
16
|
+
>
|
|
17
|
+
<slot />
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
|
|
21
|
+
<style scoped>
|
|
22
|
+
.mg-frame {
|
|
23
|
+
box-sizing: border-box;
|
|
24
|
+
background: var(--miragon-white);
|
|
25
|
+
border: 1px solid #e5e7eb;
|
|
26
|
+
border-radius: 1.1rem;
|
|
27
|
+
box-shadow: 0 8px 20px rgba(51, 93, 229, 0.08);
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
}
|
|
33
|
+
.mg-frame--compact { padding: 0.75rem; }
|
|
34
|
+
.mg-frame--standard { padding: 1rem 1.25rem; }
|
|
35
|
+
.mg-frame--generous { padding: 1.75rem 2rem; }
|
|
36
|
+
</style>
|
package/layouts/bpmn.vue
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* default — optional caption / explanatory line below the diagram
|
|
22
22
|
*/
|
|
23
23
|
import { computed } from 'vue'
|
|
24
|
+
import DiagramFrame from '../components/DiagramFrame.vue'
|
|
24
25
|
|
|
25
26
|
const props = withDefaults(
|
|
26
27
|
defineProps<{
|
|
@@ -59,14 +60,14 @@ const diagramSrc = computed(() => withBase(props.diagram))
|
|
|
59
60
|
<h2 v-if="title" class="bpmn-title">{{ title }}</h2>
|
|
60
61
|
</header>
|
|
61
62
|
|
|
62
|
-
<
|
|
63
|
+
<DiagramFrame class="bpmn-canvas" padding="compact">
|
|
63
64
|
<BpmnTokenSimulation
|
|
64
65
|
v-if="diagram"
|
|
65
66
|
:bpmnFilePath="diagramSrc"
|
|
66
67
|
width="100%"
|
|
67
68
|
:height="height"
|
|
68
69
|
/>
|
|
69
|
-
</
|
|
70
|
+
</DiagramFrame>
|
|
70
71
|
|
|
71
72
|
<div v-if="$slots.default" class="bpmn-caption">
|
|
72
73
|
<slot />
|
|
@@ -129,15 +130,7 @@ const diagramSrc = computed(() => withBase(props.diagram))
|
|
|
129
130
|
.bpmn-canvas {
|
|
130
131
|
flex: 1 1 auto;
|
|
131
132
|
min-height: 0;
|
|
132
|
-
|
|
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;
|
|
133
|
+
width: 100%;
|
|
141
134
|
}
|
|
142
135
|
|
|
143
136
|
.bpmn-caption {
|
package/layouts/dmn.vue
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* default — optional caption / explanatory line below the table
|
|
27
27
|
*/
|
|
28
28
|
import { computed } from 'vue'
|
|
29
|
+
import DiagramFrame from '../components/DiagramFrame.vue'
|
|
29
30
|
|
|
30
31
|
const props = withDefaults(
|
|
31
32
|
defineProps<{
|
|
@@ -67,7 +68,7 @@ const diagramSrc = computed(() => withBase(props.diagram))
|
|
|
67
68
|
<h2 v-if="title" class="dmn-title">{{ title }}</h2>
|
|
68
69
|
</header>
|
|
69
70
|
|
|
70
|
-
<
|
|
71
|
+
<DiagramFrame class="dmn-canvas" padding="compact">
|
|
71
72
|
<DmnTable
|
|
72
73
|
v-if="diagram"
|
|
73
74
|
:dmnFilePath="diagramSrc"
|
|
@@ -77,7 +78,7 @@ const diagramSrc = computed(() => withBase(props.diagram))
|
|
|
77
78
|
:fontSize="fontSize"
|
|
78
79
|
:showAnnotations="showAnnotations"
|
|
79
80
|
/>
|
|
80
|
-
</
|
|
81
|
+
</DiagramFrame>
|
|
81
82
|
|
|
82
83
|
<div v-if="$slots.default" class="dmn-caption">
|
|
83
84
|
<slot />
|
|
@@ -140,19 +141,11 @@ const diagramSrc = computed(() => withBase(props.diagram))
|
|
|
140
141
|
.dmn-canvas {
|
|
141
142
|
flex: 1 1 auto;
|
|
142
143
|
min-height: 0;
|
|
143
|
-
|
|
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;
|
|
144
|
+
width: 100%;
|
|
152
145
|
}
|
|
153
146
|
/* 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
|
|
155
|
-
bpmn archetype frames bpmn-js. */
|
|
147
|
+
rendering intact and only frame it in the branded DiagramFrame card, exactly
|
|
148
|
+
like the bpmn archetype frames bpmn-js. */
|
|
156
149
|
|
|
157
150
|
.dmn-caption {
|
|
158
151
|
flex: 0 0 auto;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import DiagramFrame from '../components/DiagramFrame.vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
eyebrow?: string
|
|
8
|
+
accent?: 'blue' | 'green' | 'mixed'
|
|
9
|
+
diagram?: string
|
|
10
|
+
alt?: string
|
|
11
|
+
frontmatter?: Record<string, unknown>
|
|
12
|
+
}>(),
|
|
13
|
+
{ accent: 'blue' },
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
const title = computed(() => props.frontmatter?.title as string | undefined)
|
|
17
|
+
const gradientVar = computed(() => `var(--miragon-gradient-${props.accent})`)
|
|
18
|
+
const accentVar = computed(() =>
|
|
19
|
+
props.accent === 'green' ? 'var(--miragon-green-deep)' : 'var(--miragon-blue)',
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
function withBase(path?: string) {
|
|
23
|
+
if (!path) return path
|
|
24
|
+
if (/^https?:\/\//.test(path)) return path
|
|
25
|
+
return import.meta.env.BASE_URL.replace(/\/$/, '') + '/' + path.replace(/^\//, '')
|
|
26
|
+
}
|
|
27
|
+
const imageSrc = computed(() => withBase(props.diagram))
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div class="excalidraw-layout" :style="{ '--ex-grad': gradientVar, '--ex-accent': accentVar }">
|
|
32
|
+
<div class="excalidraw-inner">
|
|
33
|
+
<header v-if="title || eyebrow" class="excalidraw-head">
|
|
34
|
+
<span class="excalidraw-bar" aria-hidden="true"></span>
|
|
35
|
+
<div v-if="eyebrow" class="excalidraw-eyebrow">{{ eyebrow }}</div>
|
|
36
|
+
<h2 v-if="title" class="excalidraw-title">{{ title }}</h2>
|
|
37
|
+
</header>
|
|
38
|
+
|
|
39
|
+
<DiagramFrame class="excalidraw-canvas" padding="generous">
|
|
40
|
+
<img v-if="imageSrc" :src="imageSrc" :alt="alt" class="excalidraw-img" />
|
|
41
|
+
</DiagramFrame>
|
|
42
|
+
|
|
43
|
+
<div v-if="$slots.default" class="excalidraw-caption">
|
|
44
|
+
<slot />
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
|
49
|
+
|
|
50
|
+
<style scoped>
|
|
51
|
+
.excalidraw-layout {
|
|
52
|
+
position: relative;
|
|
53
|
+
width: 100%;
|
|
54
|
+
height: 100%;
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
background: var(--miragon-gray-bg);
|
|
57
|
+
color: var(--miragon-text-primary);
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: stretch;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.excalidraw-inner {
|
|
63
|
+
position: relative;
|
|
64
|
+
z-index: 1;
|
|
65
|
+
width: 100%;
|
|
66
|
+
max-width: 78rem;
|
|
67
|
+
margin: 0 auto;
|
|
68
|
+
padding: 2.5rem 4rem;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.excalidraw-head {
|
|
74
|
+
flex: 0 0 auto;
|
|
75
|
+
margin-bottom: 1.25rem;
|
|
76
|
+
}
|
|
77
|
+
.excalidraw-bar {
|
|
78
|
+
display: block;
|
|
79
|
+
width: 3.5rem;
|
|
80
|
+
height: 0.35rem;
|
|
81
|
+
border-radius: 999px;
|
|
82
|
+
background: var(--ex-grad);
|
|
83
|
+
margin-bottom: 0.9rem;
|
|
84
|
+
}
|
|
85
|
+
.excalidraw-eyebrow {
|
|
86
|
+
font-size: 0.9rem;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
letter-spacing: 0.18em;
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
color: var(--ex-accent);
|
|
91
|
+
margin-bottom: 0.55rem;
|
|
92
|
+
}
|
|
93
|
+
.excalidraw-title {
|
|
94
|
+
font-size: clamp(1.7rem, 2.7vw, 2.2rem);
|
|
95
|
+
line-height: 1.15;
|
|
96
|
+
font-weight: 800;
|
|
97
|
+
letter-spacing: -0.02em;
|
|
98
|
+
margin: 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.excalidraw-canvas {
|
|
102
|
+
flex: 1 1 auto;
|
|
103
|
+
min-height: 0;
|
|
104
|
+
width: 100%;
|
|
105
|
+
}
|
|
106
|
+
.excalidraw-img {
|
|
107
|
+
max-width: 100%;
|
|
108
|
+
max-height: 100%;
|
|
109
|
+
height: auto;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.excalidraw-caption {
|
|
113
|
+
flex: 0 0 auto;
|
|
114
|
+
margin-top: 1rem;
|
|
115
|
+
font-size: 0.95rem;
|
|
116
|
+
color: var(--miragon-text-muted);
|
|
117
|
+
text-align: center;
|
|
118
|
+
}
|
|
119
|
+
.excalidraw-caption :deep(p) {
|
|
120
|
+
margin: 0;
|
|
121
|
+
line-height: 1.5;
|
|
122
|
+
}
|
|
123
|
+
.excalidraw-caption :deep(:not(pre) > code) {
|
|
124
|
+
font-family: var(--miragon-font-mono);
|
|
125
|
+
font-size: 0.9em;
|
|
126
|
+
background: var(--miragon-blue-light);
|
|
127
|
+
color: var(--miragon-blue-darker);
|
|
128
|
+
padding: 0.1em 0.4em;
|
|
129
|
+
border-radius: 0.35rem;
|
|
130
|
+
}
|
|
131
|
+
</style>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import DiagramFrame from '../components/DiagramFrame.vue'
|
|
4
|
+
|
|
5
|
+
const props = withDefaults(
|
|
6
|
+
defineProps<{
|
|
7
|
+
eyebrow?: string
|
|
8
|
+
accent?: 'blue' | 'green' | 'mixed'
|
|
9
|
+
frontmatter?: Record<string, unknown>
|
|
10
|
+
}>(),
|
|
11
|
+
{ accent: 'blue' },
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const title = computed(() => props.frontmatter?.title as string | undefined)
|
|
15
|
+
const gradientVar = computed(() => `var(--miragon-gradient-${props.accent})`)
|
|
16
|
+
const accentVar = computed(() =>
|
|
17
|
+
props.accent === 'green' ? 'var(--miragon-green-deep)' : 'var(--miragon-blue)',
|
|
18
|
+
)
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<template>
|
|
22
|
+
<div class="mermaid-layout" :style="{ '--mm-grad': gradientVar, '--mm-accent': accentVar }">
|
|
23
|
+
<div class="mermaid-inner">
|
|
24
|
+
<header v-if="title || eyebrow" class="mermaid-head">
|
|
25
|
+
<span class="mermaid-bar" aria-hidden="true"></span>
|
|
26
|
+
<div v-if="eyebrow" class="mermaid-eyebrow">{{ eyebrow }}</div>
|
|
27
|
+
<h2 v-if="title" class="mermaid-title">{{ title }}</h2>
|
|
28
|
+
</header>
|
|
29
|
+
|
|
30
|
+
<DiagramFrame class="mermaid-canvas">
|
|
31
|
+
<slot />
|
|
32
|
+
</DiagramFrame>
|
|
33
|
+
|
|
34
|
+
<div v-if="$slots.caption" class="mermaid-caption">
|
|
35
|
+
<slot name="caption" />
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<style scoped>
|
|
42
|
+
.mermaid-layout {
|
|
43
|
+
position: relative;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
overflow: hidden;
|
|
47
|
+
background: var(--miragon-gray-bg);
|
|
48
|
+
color: var(--miragon-text-primary);
|
|
49
|
+
display: flex;
|
|
50
|
+
align-items: stretch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.mermaid-inner {
|
|
54
|
+
position: relative;
|
|
55
|
+
z-index: 1;
|
|
56
|
+
width: 100%;
|
|
57
|
+
max-width: 78rem;
|
|
58
|
+
margin: 0 auto;
|
|
59
|
+
padding: 2.5rem 4rem;
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.mermaid-head {
|
|
65
|
+
flex: 0 0 auto;
|
|
66
|
+
margin-bottom: 1.25rem;
|
|
67
|
+
}
|
|
68
|
+
.mermaid-bar {
|
|
69
|
+
display: block;
|
|
70
|
+
width: 3.5rem;
|
|
71
|
+
height: 0.35rem;
|
|
72
|
+
border-radius: 999px;
|
|
73
|
+
background: var(--mm-grad);
|
|
74
|
+
margin-bottom: 0.9rem;
|
|
75
|
+
}
|
|
76
|
+
.mermaid-eyebrow {
|
|
77
|
+
font-size: 0.9rem;
|
|
78
|
+
font-weight: 600;
|
|
79
|
+
letter-spacing: 0.18em;
|
|
80
|
+
text-transform: uppercase;
|
|
81
|
+
color: var(--mm-accent);
|
|
82
|
+
margin-bottom: 0.55rem;
|
|
83
|
+
}
|
|
84
|
+
.mermaid-title {
|
|
85
|
+
font-size: clamp(1.7rem, 2.7vw, 2.2rem);
|
|
86
|
+
line-height: 1.15;
|
|
87
|
+
font-weight: 800;
|
|
88
|
+
letter-spacing: -0.02em;
|
|
89
|
+
margin: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.mermaid-canvas {
|
|
93
|
+
flex: 1 1 auto;
|
|
94
|
+
min-height: 0;
|
|
95
|
+
width: 100%;
|
|
96
|
+
}
|
|
97
|
+
.mermaid-canvas :deep(.mermaid),
|
|
98
|
+
.mermaid-canvas :deep(.mermaid-svg-wrapper) {
|
|
99
|
+
display: flex;
|
|
100
|
+
align-items: center;
|
|
101
|
+
justify-content: center;
|
|
102
|
+
width: 100%;
|
|
103
|
+
height: 100%;
|
|
104
|
+
}
|
|
105
|
+
.mermaid-canvas :deep(svg) {
|
|
106
|
+
max-width: 100%;
|
|
107
|
+
max-height: 100%;
|
|
108
|
+
height: auto;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.mermaid-caption {
|
|
112
|
+
flex: 0 0 auto;
|
|
113
|
+
margin-top: 1rem;
|
|
114
|
+
font-size: 0.95rem;
|
|
115
|
+
color: var(--miragon-text-muted);
|
|
116
|
+
text-align: center;
|
|
117
|
+
}
|
|
118
|
+
.mermaid-caption :deep(p) {
|
|
119
|
+
margin: 0;
|
|
120
|
+
line-height: 1.5;
|
|
121
|
+
}
|
|
122
|
+
.mermaid-caption :deep(:not(pre) > code) {
|
|
123
|
+
font-family: var(--miragon-font-mono);
|
|
124
|
+
font-size: 0.9em;
|
|
125
|
+
background: var(--miragon-blue-light);
|
|
126
|
+
color: var(--miragon-blue-darker);
|
|
127
|
+
padding: 0.1em 0.4em;
|
|
128
|
+
border-radius: 0.35rem;
|
|
129
|
+
}
|
|
130
|
+
</style>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miragon/slidev-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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.
|
|
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
|
],
|
package/setup/mermaid.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineMermaidSetup } from '@slidev/types'
|
|
2
|
+
|
|
3
|
+
export default defineMermaidSetup(() => ({
|
|
4
|
+
theme: 'base',
|
|
5
|
+
themeVariables: {
|
|
6
|
+
primaryColor: '#F0F4FF',
|
|
7
|
+
primaryBorderColor: '#335DE5',
|
|
8
|
+
primaryTextColor: '#000000',
|
|
9
|
+
|
|
10
|
+
lineColor: '#335DE5',
|
|
11
|
+
edgeLabelBackground: '#F5F7FF',
|
|
12
|
+
|
|
13
|
+
secondaryColor: '#F5F7FF',
|
|
14
|
+
secondaryBorderColor: '#2B4ACB',
|
|
15
|
+
tertiaryColor: '#FFFFFF',
|
|
16
|
+
tertiaryBorderColor: '#335DE5',
|
|
17
|
+
|
|
18
|
+
fontFamily: "'Geist', 'Inter', 'Helvetica Neue', Arial, sans-serif",
|
|
19
|
+
},
|
|
20
|
+
themeCSS: `
|
|
21
|
+
.node rect, .node polygon, .cluster rect { rx: 8px; ry: 8px; }
|
|
22
|
+
rect.actor, .actor rect, rect.note, .note rect,
|
|
23
|
+
.classGroup rect, .stateGroup rect, .entityBox { rx: 8px; ry: 8px; }
|
|
24
|
+
`,
|
|
25
|
+
}))
|