@troshab/slidev-theme-troshab 0.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.
- package/CLAUDE.md +537 -0
- package/LICENSE +134 -0
- package/README.md +168 -0
- package/SKILL.md +414 -0
- package/components/AnimatedCounter.vue +35 -0
- package/components/Background.vue +204 -0
- package/components/Callout.vue +135 -0
- package/components/Card.vue +75 -0
- package/components/CardGrid.vue +67 -0
- package/components/CaseStudy.vue +66 -0
- package/components/CodeDiff.vue +229 -0
- package/components/CodeHighlight.vue +337 -0
- package/components/ColorSwatch.vue +114 -0
- package/components/Confetti.vue +292 -0
- package/components/Conversation.vue +405 -0
- package/components/Countdown.vue +476 -0
- package/components/Definition.vue +59 -0
- package/components/DeviceMockup.vue +392 -0
- package/components/Funnel.vue +87 -0
- package/components/Icon.vue +73 -0
- package/components/Iframe.vue +38 -0
- package/components/Image.vue +69 -0
- package/components/ImageCompare.vue +436 -0
- package/components/MatrixGrid.vue +85 -0
- package/components/MermaidChart.vue +299 -0
- package/components/Metric.vue +161 -0
- package/components/PersonCard.vue +165 -0
- package/components/PricingTable.vue +144 -0
- package/components/Progress.vue +100 -0
- package/components/Pyramid.vue +81 -0
- package/components/QRCode.vue +137 -0
- package/components/QuoteBlock.vue +101 -0
- package/components/SpeechBubble.vue +169 -0
- package/components/Stepper.vue +542 -0
- package/components/StyledList.vue +156 -0
- package/components/StyledText.vue +275 -0
- package/components/SwotGrid.vue +99 -0
- package/components/Tags.vue +20 -0
- package/components/Testimonial.vue +243 -0
- package/components/Typewriter.vue +181 -0
- package/components_base/AnimatedCounter.vue +208 -0
- package/components_base/CodeHighlight.vue +364 -0
- package/composables/useColors.ts +101 -0
- package/composables/useShiki.ts +81 -0
- package/example_content.md +371 -0
- package/example_dark.md +10 -0
- package/example_slides/001-cover.md +15 -0
- package/example_slides/002-agenda.md +25 -0
- package/example_slides/003-section-layouts.md +14 -0
- package/example_slides/004-fullscreen-centered.md +7 -0
- package/example_slides/005-fullscreen-align-bottom.md +14 -0
- package/example_slides/006-fullscreen-no-padding.md +14 -0
- package/example_slides/007-fullscreen-bg-image-dark.md +13 -0
- package/example_slides/008-fullscreen-bg-image-light.md +13 -0
- package/example_slides/009-fullscreen-bg-gradient.md +15 -0
- package/example_slides/010-fullscreen-bg-color.md +13 -0
- package/example_slides/011-split-basic.md +17 -0
- package/example_slides/012-split-image-text.md +18 -0
- package/example_slides/013-split-contrast.md +22 -0
- package/example_slides/014-columns-basic.md +13 -0
- package/example_slides/015-columns-two.md +26 -0
- package/example_slides/016-columns-ratios.md +22 -0
- package/example_slides/017-columns-three.md +31 -0
- package/example_slides/018-columns-four.md +22 -0
- package/example_slides/019-columns-alignment.md +23 -0
- package/example_slides/020-columns-styled.md +21 -0
- package/example_slides/021-footnote-prop.md +16 -0
- package/example_slides/022-iframe-fullscreen.md +8 -0
- package/example_slides/023-iframe-split.md +18 -0
- package/example_slides/024-section-components.md +14 -0
- package/example_slides/025-styled-text.md +9 -0
- package/example_slides/026-styled-text.md +15 -0
- package/example_slides/027-text-formatting.md +28 -0
- package/example_slides/028-text-spoiler.md +15 -0
- package/example_slides/029-icon-component.md +47 -0
- package/example_slides/030-metric-component.md +29 -0
- package/example_slides/031-person-card.md +33 -0
- package/example_slides/032-styled-list.md +50 -0
- package/example_slides/033-color-swatch.md +35 -0
- package/example_slides/034-code-highlight.md +9 -0
- package/example_slides/035-iframe-component.md +9 -0
- package/example_slides/036-callout.md +15 -0
- package/example_slides/037-card-grid.md +27 -0
- package/example_slides/038-stepper-variants.md +18 -0
- package/example_slides/039-stepper-clicks.md +49 -0
- package/example_slides/040-stepper-interactive.md +28 -0
- package/example_slides/041-tags-progress.md +21 -0
- package/example_slides/042-speech-bubble.md +30 -0
- package/example_slides/043-conversation.md +13 -0
- package/example_slides/044-device-iphone.md +26 -0
- package/example_slides/045-device-browser.md +7 -0
- package/example_slides/046-qrcode.md +26 -0
- package/example_slides/047-countdown.md +14 -0
- package/example_slides/048-typewriter.md +8 -0
- package/example_slides/049-confetti.md +16 -0
- package/example_slides/050-image-compare.md +13 -0
- package/example_slides/051-code-diff.md +24 -0
- package/example_slides/052-quote-block.md +8 -0
- package/example_slides/053-testimonial.md +26 -0
- package/example_slides/054-testimonial-featured.md +16 -0
- package/example_slides/055-funnel.md +12 -0
- package/example_slides/056-pyramid.md +13 -0
- package/example_slides/057-pricing-table.md +9 -0
- package/example_slides/058-swot-grid.md +12 -0
- package/example_slides/059-matrix-grid.md +12 -0
- package/example_slides/060-case-study.md +11 -0
- package/example_slides/061-definition.md +15 -0
- package/example_slides/062-mermaid-intro.md +34 -0
- package/example_slides/063-mermaid-flowchart.md +19 -0
- package/example_slides/064-mermaid-sequence.md +17 -0
- package/example_slides/065-mermaid-xy-chart.md +16 -0
- package/example_slides/066-mermaid-pie.md +17 -0
- package/example_slides/067-mermaid-class.md +19 -0
- package/example_slides/068-mermaid-state.md +19 -0
- package/example_slides/069-mermaid-er.md +22 -0
- package/example_slides/070-mermaid-gantt.md +24 -0
- package/example_slides/071-mermaid-timeline.md +17 -0
- package/example_slides/072-mermaid-mindmap.md +21 -0
- package/example_slides/073-mermaid-gitgraph.md +20 -0
- package/example_slides/074-mermaid-split.md +30 -0
- package/example_slides/075-mermaid-columns.md +32 -0
- package/example_slides/076-section-addons.md +14 -0
- package/example_slides/077-asciinema.md +27 -0
- package/example_slides/078-fancyarrow.md +31 -0
- package/example_slides/079-fancyarrow-demo.md +23 -0
- package/example_slides/080-section-theme.md +14 -0
- package/example_slides/081-color-architecture.md +22 -0
- package/example_slides/082-semantic-text-colors.md +25 -0
- package/example_slides/083-typography.md +16 -0
- package/example_slides/084-typography-rationale.md +22 -0
- package/example_slides/085-icons.md +24 -0
- package/example_slides/086-tables.md +14 -0
- package/example_slides/087-code-blocks.md +18 -0
- package/example_slides/088-motion-modes.md +35 -0
- package/example_slides/089-slide-transitions.md +31 -0
- package/example_slides/090-v-click-reveals.md +40 -0
- package/example_slides/091-accessibility.md +27 -0
- package/example_slides/092-safe-zone.md +17 -0
- package/example_slides/093-questions.md +8 -0
- package/example_white.md +10 -0
- package/fonts/IBMPlexMono-Medium.woff2 +1449 -0
- package/fonts/IBMPlexMono-Regular.woff2 +1449 -0
- package/fonts/IBMPlexSans-Bold.woff2 +1449 -0
- package/fonts/IBMPlexSans-Medium.woff2 +1449 -0
- package/fonts/IBMPlexSans-Regular.woff2 +1449 -0
- package/fonts/IBMPlexSans-SemiBold.woff2 +1449 -0
- package/fonts/LICENSE.txt +93 -0
- package/layouts/slide.vue +251 -0
- package/package.json +62 -0
- package/public/avatars/alice.png +0 -0
- package/public/avatars/bob.png +0 -0
- package/public/avatars/carol.png +0 -0
- package/scripts/chart-audit.mjs +216 -0
- package/scripts/contrast-audit.mjs +299 -0
- package/scripts/generate-palette.mjs +395 -0
- package/scripts/integrity-audit.mjs +357 -0
- package/scripts/shared/css-utils.mjs +216 -0
- package/scripts/shiki-audit.mjs +300 -0
- package/scripts/typography-audit.mjs +300 -0
- package/setup/main.ts +107 -0
- package/setup/mermaid.ts +237 -0
- package/setup/shiki.ts +40 -0
- package/snippets/demo.ts +26 -0
- package/styles/base.css +1053 -0
- package/styles/colors.css +422 -0
- package/styles/index.css +12 -0
- package/styles/motion.css +486 -0
- package/uno.config.ts +67 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* MermaidChart — semantic wrapper for Mermaid diagrams.
|
|
4
|
+
*
|
|
5
|
+
* Provides LLMs with typed props and constraints for correct diagram usage.
|
|
6
|
+
* The actual diagram is passed as slot content containing a Mermaid code block.
|
|
7
|
+
* Renders via Slidev's built-in Mermaid support.
|
|
8
|
+
*
|
|
9
|
+
* In development mode, validates diagram complexity against recommended limits
|
|
10
|
+
* and shows a warning banner when exceeded.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* <MermaidChart type="flowchart" direction="LR">
|
|
14
|
+
*
|
|
15
|
+
* ```mermaid
|
|
16
|
+
* graph LR
|
|
17
|
+
* A[App] --> B[Server]
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* </MermaidChart>
|
|
21
|
+
*/
|
|
22
|
+
import { computed, onMounted, ref, useSlots, type VNode } from 'vue'
|
|
23
|
+
|
|
24
|
+
const props = defineProps<{
|
|
25
|
+
/** Diagram type — determines rendering constraints */
|
|
26
|
+
type: 'flowchart' | 'xychart' | 'sequence' | 'pie' | 'class' | 'state' | 'er' | 'gantt' | 'timeline' | 'mindmap' | 'gitgraph'
|
|
27
|
+
/** Flow direction for flowcharts */
|
|
28
|
+
direction?: 'LR' | 'TD' | 'BT' | 'RL'
|
|
29
|
+
/** Caption text below the diagram */
|
|
30
|
+
caption?: string
|
|
31
|
+
}>()
|
|
32
|
+
|
|
33
|
+
const isDev = import.meta.env.DEV
|
|
34
|
+
const slots = useSlots()
|
|
35
|
+
|
|
36
|
+
/** Limits per diagram type */
|
|
37
|
+
const limits: Record<string, { max: number, label: string }> = {
|
|
38
|
+
pie: { max: 5, label: 'segments' },
|
|
39
|
+
flowchart: { max: 8, label: 'nodes' },
|
|
40
|
+
xychart: { max: 8, label: 'x-axis labels' },
|
|
41
|
+
sequence: { max: 4, label: 'participants' },
|
|
42
|
+
class: { max: 5, label: 'classes' },
|
|
43
|
+
state: { max: 8, label: 'states' },
|
|
44
|
+
er: { max: 4, label: 'entities' },
|
|
45
|
+
gantt: { max: 6, label: 'tasks' },
|
|
46
|
+
timeline: { max: 6, label: 'events' },
|
|
47
|
+
mindmap: { max: 4, label: 'branches' },
|
|
48
|
+
gitgraph: { max: 8, label: 'commits' },
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Walk slot VNodes to find mermaid source code.
|
|
53
|
+
* Slidev compiles ```mermaid blocks into components with a `code` prop.
|
|
54
|
+
*/
|
|
55
|
+
function extractCode(): string | null {
|
|
56
|
+
const vnodes = slots.default?.()
|
|
57
|
+
if (!vnodes) return null
|
|
58
|
+
|
|
59
|
+
function walk(nodes: VNode[]): string | null {
|
|
60
|
+
for (const vn of nodes) {
|
|
61
|
+
if (typeof vn.props?.code === 'string') return vn.props.code
|
|
62
|
+
if (Array.isArray(vn.children)) {
|
|
63
|
+
const found = walk(vn.children as VNode[])
|
|
64
|
+
if (found) return found
|
|
65
|
+
}
|
|
66
|
+
if (vn.component?.props?.code) {
|
|
67
|
+
return vn.component.props.code as string
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return walk(vnodes)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Count items in mermaid code based on diagram type */
|
|
77
|
+
function countItems(type: string, code: string): number {
|
|
78
|
+
switch (type) {
|
|
79
|
+
case 'pie':
|
|
80
|
+
return (code.match(/".+?"\s*:/g) || []).length
|
|
81
|
+
|
|
82
|
+
case 'flowchart': {
|
|
83
|
+
// Match node IDs before shape brackets: A[text], B(text), C{text}, D((text)), etc.
|
|
84
|
+
const matches = code.match(/(?:^|\s|-->|---)([A-Za-z]\w*)[\[({]/gm) || []
|
|
85
|
+
const ids = new Set(matches.map(m => {
|
|
86
|
+
const id = m.replace(/^[\s\->]+/, '').replace(/[\[({]$/, '')
|
|
87
|
+
return id
|
|
88
|
+
}))
|
|
89
|
+
return ids.size
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
case 'sequence': {
|
|
93
|
+
const actors = new Set<string>()
|
|
94
|
+
for (const m of code.matchAll(/(\S+)\s*->>?\+?\s*(\S+)/g)) {
|
|
95
|
+
actors.add(m[1].replace(/:$/, ''))
|
|
96
|
+
actors.add(m[2].replace(/:$/, ''))
|
|
97
|
+
}
|
|
98
|
+
return actors.size
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
case 'xychart': {
|
|
102
|
+
// x-axis [...labels...] or x-axis "title" [...labels...]
|
|
103
|
+
const xMatch = code.match(/x-axis\s+(?:"[^"]*"\s+)?\[([^\]]+)\]/)
|
|
104
|
+
return xMatch ? xMatch[1].split(',').length : 0
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
case 'class':
|
|
108
|
+
return (code.match(/^\s*class\s+\w+/gm) || []).length
|
|
109
|
+
|
|
110
|
+
case 'state':
|
|
111
|
+
// Count state definitions (lines with "state" keyword or [*] transitions)
|
|
112
|
+
return new Set(
|
|
113
|
+
(code.match(/(?:state\s+"[^"]*"\s+as\s+(\w+)|\b(\w+)\s*-->)/gm) || [])
|
|
114
|
+
.map(m => m.replace(/\s*-->.*/, '').replace(/^state\s+"[^"]*"\s+as\s+/, '').trim())
|
|
115
|
+
.filter(Boolean)
|
|
116
|
+
).size
|
|
117
|
+
|
|
118
|
+
case 'er':
|
|
119
|
+
// Entity names before { blocks
|
|
120
|
+
return (code.match(/^\s*\w[\w-]*\s*\{/gm) || []).length
|
|
121
|
+
|
|
122
|
+
case 'gantt':
|
|
123
|
+
// Task lines: indented lines that are not section/title/dateFormat/axisFormat/etc.
|
|
124
|
+
return (code.match(/^\s{2,}\w[^:]*:[^,\n]*/gm) || []).length
|
|
125
|
+
|
|
126
|
+
case 'timeline': {
|
|
127
|
+
// Period entries: lines with content : detail
|
|
128
|
+
return (code.match(/^\s+.+\s*:\s*.+$/gm) || []).length
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'mindmap': {
|
|
132
|
+
// First-level branches: lines with exactly one level of indentation
|
|
133
|
+
const lines = code.split('\n')
|
|
134
|
+
let rootIndent = -1
|
|
135
|
+
let branches = 0
|
|
136
|
+
for (const line of lines) {
|
|
137
|
+
if (!line.trim() || line.trim().startsWith('mindmap')) continue
|
|
138
|
+
const indent = line.search(/\S/)
|
|
139
|
+
if (rootIndent === -1) { rootIndent = indent; continue }
|
|
140
|
+
if (indent === rootIndent + 2 || indent === rootIndent + 4) branches++
|
|
141
|
+
}
|
|
142
|
+
return branches
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
case 'gitgraph':
|
|
146
|
+
return (code.match(/^\s*commit/gm) || []).length
|
|
147
|
+
|
|
148
|
+
default:
|
|
149
|
+
return 0
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const diagramRef = ref<HTMLElement | null>(null)
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Mermaid renders SVG inside a shadow DOM with an inline max-width
|
|
157
|
+
* that caps the diagram at its intrinsic size. We observe the shadow root
|
|
158
|
+
* and remove that constraint so the SVG scales to fill the container.
|
|
159
|
+
*/
|
|
160
|
+
onMounted(() => {
|
|
161
|
+
const el = diagramRef.value
|
|
162
|
+
if (!el) return
|
|
163
|
+
|
|
164
|
+
function unlockSvg(mermaidEl: Element) {
|
|
165
|
+
const sr = (mermaidEl as any).shadowRoot
|
|
166
|
+
if (!sr) return
|
|
167
|
+
const svg = sr.querySelector('svg')
|
|
168
|
+
if (svg) {
|
|
169
|
+
svg.removeAttribute('height')
|
|
170
|
+
svg.style.maxWidth = 'none'
|
|
171
|
+
svg.style.width = '100%'
|
|
172
|
+
svg.style.height = 'auto'
|
|
173
|
+
svg.style.maxHeight = '100%'
|
|
174
|
+
// Mermaid renders text as foreignObject with overflow:hidden — unclip
|
|
175
|
+
sr.querySelectorAll('foreignObject').forEach((fo: Element) => {
|
|
176
|
+
;(fo as HTMLElement).style.overflow = 'visible'
|
|
177
|
+
})
|
|
178
|
+
// Mindmap nodes use dark cScale chart backgrounds — Mermaid's textColor
|
|
179
|
+
// applies globally and has no mindmap-specific override, so fix via CSS var
|
|
180
|
+
if (props.type === 'mindmap') {
|
|
181
|
+
sr.querySelectorAll('foreignObject div').forEach((el: Element) => {
|
|
182
|
+
;(el as HTMLElement).style.color = 'var(--color-bg)'
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
// Gantt critText: Mermaid has no themeVariable for critText (only
|
|
186
|
+
// taskTextColor applies, which can't differ from regular task text).
|
|
187
|
+
// Avoid using `crit` tasks in demos, or accept the contrast limitation.
|
|
188
|
+
// done/active text is handled by taskTextDarkColor in mermaid.ts.
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Observe shadow root for SVG changes (initial render + re-renders).
|
|
194
|
+
* Mermaid may re-render after initial mount (e.g. theme config applied),
|
|
195
|
+
* so we must re-apply fixes each time a new SVG appears.
|
|
196
|
+
*/
|
|
197
|
+
function observeShadowRoot(mermaidEl: Element) {
|
|
198
|
+
const sr = (mermaidEl as any).shadowRoot
|
|
199
|
+
if (!sr) return
|
|
200
|
+
|
|
201
|
+
// Apply immediately if SVG already exists
|
|
202
|
+
if (sr.querySelector('svg')) unlockSvg(mermaidEl)
|
|
203
|
+
|
|
204
|
+
// Watch for SVG additions/replacements in shadow root
|
|
205
|
+
const srObserver = new MutationObserver(() => {
|
|
206
|
+
if (sr.querySelector('svg')) unlockSvg(mermaidEl)
|
|
207
|
+
})
|
|
208
|
+
srObserver.observe(sr, { childList: true, subtree: true })
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Poll until .mermaid element has a shadow root, then observe it. */
|
|
212
|
+
function pollForShadowRoot(mermaidEl: Element) {
|
|
213
|
+
if ((mermaidEl as any).shadowRoot) {
|
|
214
|
+
observeShadowRoot(mermaidEl)
|
|
215
|
+
} else {
|
|
216
|
+
requestAnimationFrame(() => pollForShadowRoot(mermaidEl))
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// If .mermaid already in DOM (Slidev pre-render), start observing
|
|
221
|
+
const existing = el.querySelector('.mermaid')
|
|
222
|
+
if (existing) pollForShadowRoot(existing)
|
|
223
|
+
|
|
224
|
+
// Watch for .mermaid being added later (lazy rendering)
|
|
225
|
+
const observer = new MutationObserver(() => {
|
|
226
|
+
const mermaidEl = el.querySelector('.mermaid')
|
|
227
|
+
if (mermaidEl) pollForShadowRoot(mermaidEl)
|
|
228
|
+
})
|
|
229
|
+
observer.observe(el, { childList: true, subtree: true })
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const warning = computed<string | null>(() => {
|
|
233
|
+
if (!isDev) return null
|
|
234
|
+
const code = extractCode()
|
|
235
|
+
if (!code) return null
|
|
236
|
+
const limit = limits[props.type]
|
|
237
|
+
if (!limit) return null
|
|
238
|
+
const count = countItems(props.type, code)
|
|
239
|
+
if (count > limit.max) {
|
|
240
|
+
return `${props.type}: ${count} ${limit.label} (max ${limit.max})`
|
|
241
|
+
}
|
|
242
|
+
return null
|
|
243
|
+
})
|
|
244
|
+
</script>
|
|
245
|
+
|
|
246
|
+
<template>
|
|
247
|
+
<div class="mermaid-chart">
|
|
248
|
+
<div v-if="warning" class="mermaid-chart-warning">{{ warning }}</div>
|
|
249
|
+
<div ref="diagramRef" class="mermaid-chart-diagram">
|
|
250
|
+
<slot />
|
|
251
|
+
</div>
|
|
252
|
+
<p v-if="caption" class="mermaid-chart-caption">{{ caption }}</p>
|
|
253
|
+
</div>
|
|
254
|
+
</template>
|
|
255
|
+
|
|
256
|
+
<style scoped>
|
|
257
|
+
.mermaid-chart {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-direction: column;
|
|
260
|
+
align-items: center;
|
|
261
|
+
justify-content: center;
|
|
262
|
+
gap: var(--space-sm);
|
|
263
|
+
width: 100%;
|
|
264
|
+
flex: 1;
|
|
265
|
+
min-height: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.mermaid-chart-diagram {
|
|
269
|
+
width: 100%;
|
|
270
|
+
flex: 0 1 auto;
|
|
271
|
+
min-height: 0;
|
|
272
|
+
display: flex;
|
|
273
|
+
justify-content: center;
|
|
274
|
+
align-items: center;
|
|
275
|
+
overflow: hidden;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.mermaid-chart-diagram :deep(.mermaid) {
|
|
279
|
+
width: 100%;
|
|
280
|
+
height: 100%;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.mermaid-chart-warning {
|
|
284
|
+
align-self: stretch;
|
|
285
|
+
padding: 0.375rem 0.75rem;
|
|
286
|
+
border-left: 3px solid var(--color-danger);
|
|
287
|
+
background: var(--color-danger-tint);
|
|
288
|
+
color: var(--color-danger);
|
|
289
|
+
font-size: var(--font-size-small);
|
|
290
|
+
line-height: 1.4;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
.mermaid-chart-caption {
|
|
294
|
+
font-size: var(--font-size-small);
|
|
295
|
+
color: var(--color-text-secondary);
|
|
296
|
+
text-align: center;
|
|
297
|
+
margin: 0;
|
|
298
|
+
}
|
|
299
|
+
</style>
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Metric primitive — KPI/stat display with optional animation.
|
|
4
|
+
*
|
|
5
|
+
* Static: <Metric value="99.9%" label="Uptime" color="primary" />
|
|
6
|
+
* Animated: <Metric :value="10000" label="Users" color="success" animated separator="," />
|
|
7
|
+
* Full: <Metric :value="2.5" prefix="$" suffix="M" label="Revenue" color="info"
|
|
8
|
+
* animated :from="0" :duration="2500" :decimals="1" />
|
|
9
|
+
*
|
|
10
|
+
* When `animated` is set and `value` is a number, uses AnimatedCounter internally.
|
|
11
|
+
* When `value` is a string (or animated is false), displays statically.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { computed } from 'vue'
|
|
15
|
+
import type { SemanticColor } from '../composables/useColors'
|
|
16
|
+
import { semanticColorVar, gradientVar } from '../composables/useColors'
|
|
17
|
+
import AnimatedCounter from '../components_base/AnimatedCounter.vue'
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
value: string | number
|
|
21
|
+
label: string
|
|
22
|
+
color?: SemanticColor
|
|
23
|
+
prefix?: string
|
|
24
|
+
suffix?: string
|
|
25
|
+
icon?: string
|
|
26
|
+
size?: 'sm' | 'md' | 'lg'
|
|
27
|
+
variant?: 'card' | 'bare'
|
|
28
|
+
gradient?: boolean
|
|
29
|
+
// Animation props (only when value is number)
|
|
30
|
+
animated?: boolean
|
|
31
|
+
from?: number
|
|
32
|
+
duration?: number
|
|
33
|
+
decimals?: number
|
|
34
|
+
separator?: string
|
|
35
|
+
easing?: 'linear' | 'easeOut' | 'easeInOut'
|
|
36
|
+
at?: number // undefined = auto-register click, 0 = on slide enter, -1 = manual
|
|
37
|
+
}>(), {
|
|
38
|
+
color: 'primary',
|
|
39
|
+
prefix: '',
|
|
40
|
+
suffix: '',
|
|
41
|
+
size: 'md',
|
|
42
|
+
variant: 'card',
|
|
43
|
+
gradient: false,
|
|
44
|
+
animated: false,
|
|
45
|
+
from: 0,
|
|
46
|
+
duration: 2000,
|
|
47
|
+
decimals: 0,
|
|
48
|
+
separator: '',
|
|
49
|
+
easing: 'easeOut',
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const isNumeric = computed(() => typeof props.value === 'number')
|
|
53
|
+
const shouldAnimate = computed(() => props.animated && isNumeric.value)
|
|
54
|
+
|
|
55
|
+
const staticDisplay = computed(() => {
|
|
56
|
+
if (isNumeric.value && !shouldAnimate.value) {
|
|
57
|
+
let num = (props.value as number).toFixed(props.decimals)
|
|
58
|
+
if (props.separator) {
|
|
59
|
+
const parts = num.split('.')
|
|
60
|
+
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, props.separator)
|
|
61
|
+
num = parts.join('.')
|
|
62
|
+
}
|
|
63
|
+
return `${props.prefix}${num}${props.suffix}`
|
|
64
|
+
}
|
|
65
|
+
return `${props.prefix}${props.value}${props.suffix}`
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const sizeClasses: Record<string, string> = {
|
|
69
|
+
sm: 'metric-sm',
|
|
70
|
+
md: 'metric-md',
|
|
71
|
+
lg: 'metric-lg',
|
|
72
|
+
}
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div class="metric" :class="[sizeClasses[size], { 'metric-bare': variant === 'bare' }]">
|
|
77
|
+
<div v-if="$slots.icon" class="metric-icon" :style="{ color: semanticColorVar[color] }">
|
|
78
|
+
<slot name="icon" />
|
|
79
|
+
</div>
|
|
80
|
+
<div class="metric-value" :class="{ 'metric-gradient': gradient }" :style="gradient ? { '--metric-gradient': gradientVar[color] } : { color: semanticColorVar[color] }">
|
|
81
|
+
<template v-if="shouldAnimate">
|
|
82
|
+
<span v-if="prefix" class="metric-affix">{{ prefix }}</span>
|
|
83
|
+
<AnimatedCounter
|
|
84
|
+
:value="(value as number)"
|
|
85
|
+
:from="from"
|
|
86
|
+
:duration="duration"
|
|
87
|
+
:decimals="decimals"
|
|
88
|
+
:separator="separator"
|
|
89
|
+
:easing="easing"
|
|
90
|
+
:at="at"
|
|
91
|
+
/>
|
|
92
|
+
<span v-if="suffix" class="metric-affix">{{ suffix }}</span>
|
|
93
|
+
</template>
|
|
94
|
+
<template v-else>{{ staticDisplay }}</template>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="metric-label">{{ label }}</div>
|
|
97
|
+
<div v-if="$slots.default" class="metric-extra">
|
|
98
|
+
<slot />
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</template>
|
|
102
|
+
|
|
103
|
+
<style scoped>
|
|
104
|
+
.metric {
|
|
105
|
+
text-align: center;
|
|
106
|
+
padding: var(--space-md);
|
|
107
|
+
background: var(--color-bg-soft);
|
|
108
|
+
border-radius: 0.5rem;
|
|
109
|
+
border: 1px solid var(--color-border);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.metric-bare {
|
|
113
|
+
background: none;
|
|
114
|
+
border: none;
|
|
115
|
+
padding: 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.metric-icon {
|
|
119
|
+
font-size: 1.5em;
|
|
120
|
+
margin-bottom: var(--space-xs);
|
|
121
|
+
line-height: 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.metric-value {
|
|
125
|
+
font-weight: var(--font-weight-bold);
|
|
126
|
+
line-height: 1.1;
|
|
127
|
+
font-variant-numeric: tabular-nums;
|
|
128
|
+
white-space: nowrap;
|
|
129
|
+
overflow: hidden;
|
|
130
|
+
text-overflow: ellipsis;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.metric-affix {
|
|
134
|
+
font-weight: inherit;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.metric-label {
|
|
138
|
+
font-size: var(--font-size-small);
|
|
139
|
+
color: var(--color-text-secondary);
|
|
140
|
+
margin-top: var(--space-xs);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.metric-extra {
|
|
144
|
+
margin-top: var(--space-xs);
|
|
145
|
+
font-size: var(--font-size-small);
|
|
146
|
+
color: var(--color-text-tertiary);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Sizes */
|
|
150
|
+
.metric-sm .metric-value { font-size: var(--font-size-h2); }
|
|
151
|
+
.metric-md .metric-value { font-size: calc(var(--font-size-h1) * 1.1); }
|
|
152
|
+
.metric-lg .metric-value { font-size: calc(var(--font-size-h1) * 1.5); }
|
|
153
|
+
|
|
154
|
+
/* Gradient text effect */
|
|
155
|
+
.metric-gradient {
|
|
156
|
+
background: var(--metric-gradient, var(--gradient-primary));
|
|
157
|
+
-webkit-background-clip: text;
|
|
158
|
+
-webkit-text-fill-color: transparent;
|
|
159
|
+
background-clip: text;
|
|
160
|
+
}
|
|
161
|
+
</style>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* PersonCard primitive — speaker/team member introduction.
|
|
4
|
+
*
|
|
5
|
+
* <PersonCard name="John Doe" role="CTO" initials="JD" />
|
|
6
|
+
* <PersonCard name="Jane" role="Engineer" company="Acme" avatar="/photo.jpg" />
|
|
7
|
+
* <PersonCard name="Alex" role="Designer" bio="10 years experience" horizontal />
|
|
8
|
+
* <PersonCard name="Sam" role="Lead" size="sm" />
|
|
9
|
+
*
|
|
10
|
+
* Auto-generates initials from name if not provided.
|
|
11
|
+
* Supports avatar image, initials circle, or no avatar.
|
|
12
|
+
* Default slot for extra content (links, badges, etc).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { computed } from 'vue'
|
|
16
|
+
import type { ExtendedColor } from '../composables/useColors'
|
|
17
|
+
import { gradientVar } from '../composables/useColors'
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<{
|
|
20
|
+
name: string
|
|
21
|
+
role?: string
|
|
22
|
+
company?: string
|
|
23
|
+
avatar?: string
|
|
24
|
+
initials?: string
|
|
25
|
+
bio?: string
|
|
26
|
+
size?: 'sm' | 'md' | 'lg'
|
|
27
|
+
align?: 'left' | 'center'
|
|
28
|
+
horizontal?: boolean
|
|
29
|
+
color?: ExtendedColor
|
|
30
|
+
}>(), {
|
|
31
|
+
size: 'md',
|
|
32
|
+
align: 'center',
|
|
33
|
+
horizontal: false,
|
|
34
|
+
color: 'primary',
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const autoInitials = computed(() => {
|
|
38
|
+
if (props.initials) return props.initials
|
|
39
|
+
return props.name
|
|
40
|
+
.split(/\s+/)
|
|
41
|
+
.map(w => w[0])
|
|
42
|
+
.slice(0, 2)
|
|
43
|
+
.join('')
|
|
44
|
+
.toUpperCase()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const hasAvatar = computed(() => !!props.avatar || !!props.initials || true)
|
|
48
|
+
|
|
49
|
+
const avatarBg = computed(() => gradientVar[props.color])
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<template>
|
|
53
|
+
<div
|
|
54
|
+
class="person-card"
|
|
55
|
+
:class="[
|
|
56
|
+
`person-${size}`,
|
|
57
|
+
`person-align-${align}`,
|
|
58
|
+
horizontal ? 'person-horizontal' : '',
|
|
59
|
+
]"
|
|
60
|
+
>
|
|
61
|
+
<!-- Avatar -->
|
|
62
|
+
<div class="person-avatar-wrap">
|
|
63
|
+
<img
|
|
64
|
+
v-if="avatar"
|
|
65
|
+
:src="avatar"
|
|
66
|
+
:alt="name"
|
|
67
|
+
class="person-avatar person-avatar-img"
|
|
68
|
+
/>
|
|
69
|
+
<div
|
|
70
|
+
v-else
|
|
71
|
+
class="person-avatar person-avatar-initials"
|
|
72
|
+
:style="{ background: avatarBg }"
|
|
73
|
+
>
|
|
74
|
+
{{ autoInitials }}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<!-- Info -->
|
|
79
|
+
<div class="person-info">
|
|
80
|
+
<div class="person-name">{{ name }}</div>
|
|
81
|
+
<div v-if="role || company" class="person-role">
|
|
82
|
+
{{ role }}<template v-if="role && company"> @ </template>{{ company }}
|
|
83
|
+
</div>
|
|
84
|
+
<div v-if="bio" class="person-bio">{{ bio }}</div>
|
|
85
|
+
<div v-if="$slots.default" class="person-extra">
|
|
86
|
+
<slot />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
91
|
+
|
|
92
|
+
<style scoped>
|
|
93
|
+
.person-card {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-direction: column;
|
|
96
|
+
gap: var(--space-xs);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.person-align-center { align-items: center; text-align: center; }
|
|
100
|
+
.person-align-left { align-items: flex-start; text-align: left; }
|
|
101
|
+
|
|
102
|
+
.person-horizontal {
|
|
103
|
+
flex-direction: row;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: var(--space-md);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.person-horizontal .person-info {
|
|
109
|
+
text-align: left;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* Avatar */
|
|
113
|
+
.person-avatar {
|
|
114
|
+
border-radius: 50%;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
overflow: hidden;
|
|
119
|
+
flex-shrink: 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.person-avatar-img {
|
|
123
|
+
object-fit: cover;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.person-avatar-initials {
|
|
127
|
+
color: var(--color-primary-foreground);
|
|
128
|
+
font-weight: var(--font-weight-bold);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Sizes */
|
|
132
|
+
.person-sm .person-avatar { width: 3rem; height: 3rem; font-size: 1.2rem; }
|
|
133
|
+
.person-md .person-avatar { width: 5rem; height: 5rem; font-size: 2rem; }
|
|
134
|
+
.person-lg .person-avatar { width: 7rem; height: 7rem; font-size: 2.8rem; }
|
|
135
|
+
|
|
136
|
+
.person-sm .person-name { font-size: var(--font-size-small); }
|
|
137
|
+
.person-md .person-name { font-size: var(--font-size-base); }
|
|
138
|
+
.person-lg .person-name { font-size: var(--font-size-h2); }
|
|
139
|
+
|
|
140
|
+
/* Info */
|
|
141
|
+
.person-name {
|
|
142
|
+
font-weight: var(--font-weight-semibold);
|
|
143
|
+
margin-top: var(--space-xs);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.person-horizontal .person-name {
|
|
147
|
+
margin-top: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.person-role {
|
|
151
|
+
font-size: var(--font-size-small);
|
|
152
|
+
color: var(--color-text-secondary);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.person-bio {
|
|
156
|
+
font-size: var(--font-size-small);
|
|
157
|
+
color: var(--color-text-tertiary);
|
|
158
|
+
margin-top: var(--space-xs);
|
|
159
|
+
max-width: 35ch;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.person-extra {
|
|
163
|
+
margin-top: var(--space-xs);
|
|
164
|
+
}
|
|
165
|
+
</style>
|