@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.
Files changed (168) hide show
  1. package/CLAUDE.md +537 -0
  2. package/LICENSE +134 -0
  3. package/README.md +168 -0
  4. package/SKILL.md +414 -0
  5. package/components/AnimatedCounter.vue +35 -0
  6. package/components/Background.vue +204 -0
  7. package/components/Callout.vue +135 -0
  8. package/components/Card.vue +75 -0
  9. package/components/CardGrid.vue +67 -0
  10. package/components/CaseStudy.vue +66 -0
  11. package/components/CodeDiff.vue +229 -0
  12. package/components/CodeHighlight.vue +337 -0
  13. package/components/ColorSwatch.vue +114 -0
  14. package/components/Confetti.vue +292 -0
  15. package/components/Conversation.vue +405 -0
  16. package/components/Countdown.vue +476 -0
  17. package/components/Definition.vue +59 -0
  18. package/components/DeviceMockup.vue +392 -0
  19. package/components/Funnel.vue +87 -0
  20. package/components/Icon.vue +73 -0
  21. package/components/Iframe.vue +38 -0
  22. package/components/Image.vue +69 -0
  23. package/components/ImageCompare.vue +436 -0
  24. package/components/MatrixGrid.vue +85 -0
  25. package/components/MermaidChart.vue +299 -0
  26. package/components/Metric.vue +161 -0
  27. package/components/PersonCard.vue +165 -0
  28. package/components/PricingTable.vue +144 -0
  29. package/components/Progress.vue +100 -0
  30. package/components/Pyramid.vue +81 -0
  31. package/components/QRCode.vue +137 -0
  32. package/components/QuoteBlock.vue +101 -0
  33. package/components/SpeechBubble.vue +169 -0
  34. package/components/Stepper.vue +542 -0
  35. package/components/StyledList.vue +156 -0
  36. package/components/StyledText.vue +275 -0
  37. package/components/SwotGrid.vue +99 -0
  38. package/components/Tags.vue +20 -0
  39. package/components/Testimonial.vue +243 -0
  40. package/components/Typewriter.vue +181 -0
  41. package/components_base/AnimatedCounter.vue +208 -0
  42. package/components_base/CodeHighlight.vue +364 -0
  43. package/composables/useColors.ts +101 -0
  44. package/composables/useShiki.ts +81 -0
  45. package/example_content.md +371 -0
  46. package/example_dark.md +10 -0
  47. package/example_slides/001-cover.md +15 -0
  48. package/example_slides/002-agenda.md +25 -0
  49. package/example_slides/003-section-layouts.md +14 -0
  50. package/example_slides/004-fullscreen-centered.md +7 -0
  51. package/example_slides/005-fullscreen-align-bottom.md +14 -0
  52. package/example_slides/006-fullscreen-no-padding.md +14 -0
  53. package/example_slides/007-fullscreen-bg-image-dark.md +13 -0
  54. package/example_slides/008-fullscreen-bg-image-light.md +13 -0
  55. package/example_slides/009-fullscreen-bg-gradient.md +15 -0
  56. package/example_slides/010-fullscreen-bg-color.md +13 -0
  57. package/example_slides/011-split-basic.md +17 -0
  58. package/example_slides/012-split-image-text.md +18 -0
  59. package/example_slides/013-split-contrast.md +22 -0
  60. package/example_slides/014-columns-basic.md +13 -0
  61. package/example_slides/015-columns-two.md +26 -0
  62. package/example_slides/016-columns-ratios.md +22 -0
  63. package/example_slides/017-columns-three.md +31 -0
  64. package/example_slides/018-columns-four.md +22 -0
  65. package/example_slides/019-columns-alignment.md +23 -0
  66. package/example_slides/020-columns-styled.md +21 -0
  67. package/example_slides/021-footnote-prop.md +16 -0
  68. package/example_slides/022-iframe-fullscreen.md +8 -0
  69. package/example_slides/023-iframe-split.md +18 -0
  70. package/example_slides/024-section-components.md +14 -0
  71. package/example_slides/025-styled-text.md +9 -0
  72. package/example_slides/026-styled-text.md +15 -0
  73. package/example_slides/027-text-formatting.md +28 -0
  74. package/example_slides/028-text-spoiler.md +15 -0
  75. package/example_slides/029-icon-component.md +47 -0
  76. package/example_slides/030-metric-component.md +29 -0
  77. package/example_slides/031-person-card.md +33 -0
  78. package/example_slides/032-styled-list.md +50 -0
  79. package/example_slides/033-color-swatch.md +35 -0
  80. package/example_slides/034-code-highlight.md +9 -0
  81. package/example_slides/035-iframe-component.md +9 -0
  82. package/example_slides/036-callout.md +15 -0
  83. package/example_slides/037-card-grid.md +27 -0
  84. package/example_slides/038-stepper-variants.md +18 -0
  85. package/example_slides/039-stepper-clicks.md +49 -0
  86. package/example_slides/040-stepper-interactive.md +28 -0
  87. package/example_slides/041-tags-progress.md +21 -0
  88. package/example_slides/042-speech-bubble.md +30 -0
  89. package/example_slides/043-conversation.md +13 -0
  90. package/example_slides/044-device-iphone.md +26 -0
  91. package/example_slides/045-device-browser.md +7 -0
  92. package/example_slides/046-qrcode.md +26 -0
  93. package/example_slides/047-countdown.md +14 -0
  94. package/example_slides/048-typewriter.md +8 -0
  95. package/example_slides/049-confetti.md +16 -0
  96. package/example_slides/050-image-compare.md +13 -0
  97. package/example_slides/051-code-diff.md +24 -0
  98. package/example_slides/052-quote-block.md +8 -0
  99. package/example_slides/053-testimonial.md +26 -0
  100. package/example_slides/054-testimonial-featured.md +16 -0
  101. package/example_slides/055-funnel.md +12 -0
  102. package/example_slides/056-pyramid.md +13 -0
  103. package/example_slides/057-pricing-table.md +9 -0
  104. package/example_slides/058-swot-grid.md +12 -0
  105. package/example_slides/059-matrix-grid.md +12 -0
  106. package/example_slides/060-case-study.md +11 -0
  107. package/example_slides/061-definition.md +15 -0
  108. package/example_slides/062-mermaid-intro.md +34 -0
  109. package/example_slides/063-mermaid-flowchart.md +19 -0
  110. package/example_slides/064-mermaid-sequence.md +17 -0
  111. package/example_slides/065-mermaid-xy-chart.md +16 -0
  112. package/example_slides/066-mermaid-pie.md +17 -0
  113. package/example_slides/067-mermaid-class.md +19 -0
  114. package/example_slides/068-mermaid-state.md +19 -0
  115. package/example_slides/069-mermaid-er.md +22 -0
  116. package/example_slides/070-mermaid-gantt.md +24 -0
  117. package/example_slides/071-mermaid-timeline.md +17 -0
  118. package/example_slides/072-mermaid-mindmap.md +21 -0
  119. package/example_slides/073-mermaid-gitgraph.md +20 -0
  120. package/example_slides/074-mermaid-split.md +30 -0
  121. package/example_slides/075-mermaid-columns.md +32 -0
  122. package/example_slides/076-section-addons.md +14 -0
  123. package/example_slides/077-asciinema.md +27 -0
  124. package/example_slides/078-fancyarrow.md +31 -0
  125. package/example_slides/079-fancyarrow-demo.md +23 -0
  126. package/example_slides/080-section-theme.md +14 -0
  127. package/example_slides/081-color-architecture.md +22 -0
  128. package/example_slides/082-semantic-text-colors.md +25 -0
  129. package/example_slides/083-typography.md +16 -0
  130. package/example_slides/084-typography-rationale.md +22 -0
  131. package/example_slides/085-icons.md +24 -0
  132. package/example_slides/086-tables.md +14 -0
  133. package/example_slides/087-code-blocks.md +18 -0
  134. package/example_slides/088-motion-modes.md +35 -0
  135. package/example_slides/089-slide-transitions.md +31 -0
  136. package/example_slides/090-v-click-reveals.md +40 -0
  137. package/example_slides/091-accessibility.md +27 -0
  138. package/example_slides/092-safe-zone.md +17 -0
  139. package/example_slides/093-questions.md +8 -0
  140. package/example_white.md +10 -0
  141. package/fonts/IBMPlexMono-Medium.woff2 +1449 -0
  142. package/fonts/IBMPlexMono-Regular.woff2 +1449 -0
  143. package/fonts/IBMPlexSans-Bold.woff2 +1449 -0
  144. package/fonts/IBMPlexSans-Medium.woff2 +1449 -0
  145. package/fonts/IBMPlexSans-Regular.woff2 +1449 -0
  146. package/fonts/IBMPlexSans-SemiBold.woff2 +1449 -0
  147. package/fonts/LICENSE.txt +93 -0
  148. package/layouts/slide.vue +251 -0
  149. package/package.json +62 -0
  150. package/public/avatars/alice.png +0 -0
  151. package/public/avatars/bob.png +0 -0
  152. package/public/avatars/carol.png +0 -0
  153. package/scripts/chart-audit.mjs +216 -0
  154. package/scripts/contrast-audit.mjs +299 -0
  155. package/scripts/generate-palette.mjs +395 -0
  156. package/scripts/integrity-audit.mjs +357 -0
  157. package/scripts/shared/css-utils.mjs +216 -0
  158. package/scripts/shiki-audit.mjs +300 -0
  159. package/scripts/typography-audit.mjs +300 -0
  160. package/setup/main.ts +107 -0
  161. package/setup/mermaid.ts +237 -0
  162. package/setup/shiki.ts +40 -0
  163. package/snippets/demo.ts +26 -0
  164. package/styles/base.css +1053 -0
  165. package/styles/colors.css +422 -0
  166. package/styles/index.css +12 -0
  167. package/styles/motion.css +486 -0
  168. package/uno.config.ts +67 -0
@@ -0,0 +1,169 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * SpeechBubble component - speech/thought bubbles for quotes and dialogs
4
+ *
5
+ * Usage:
6
+ * <SpeechBubble>Hello!</SpeechBubble>
7
+ * <SpeechBubble position="left" shape="thought">Hmm...</SpeechBubble>
8
+ * <SpeechBubble color="info" position="right">Note</SpeechBubble>
9
+ */
10
+
11
+ import { computed } from 'vue'
12
+ import type { SemanticColor } from '../composables/useColors'
13
+
14
+ const props = defineProps<{
15
+ position?: 'left' | 'right' | 'top' | 'bottom'
16
+ shape?: 'speech' | 'thought'
17
+ color?: 'default' | SemanticColor
18
+ maxWidth?: string
19
+ }>()
20
+
21
+ const bubbleClass = computed(() => [
22
+ 'speech-bubble',
23
+ `bubble-${props.position || 'bottom'}`,
24
+ `bubble-${props.shape || 'speech'}`,
25
+ ])
26
+
27
+ const bubbleStyle = computed(() => {
28
+ const styles: Record<string, string> = {}
29
+ if (props.maxWidth) styles.maxWidth = props.maxWidth
30
+ const c = props.color || 'default'
31
+ if (c !== 'default') {
32
+ styles['--bubble-bg'] = `var(--color-${c}-tint)`
33
+ styles['--bubble-border'] = `var(--color-${c})`
34
+ }
35
+ return styles
36
+ })
37
+ </script>
38
+
39
+ <template>
40
+ <div :class="bubbleClass" :style="bubbleStyle">
41
+ <div class="bubble-content">
42
+ <slot />
43
+ </div>
44
+ </div>
45
+ </template>
46
+
47
+ <style>
48
+ .speech-bubble {
49
+ --bubble-bg: var(--color-bg-soft);
50
+ --bubble-border: var(--color-border);
51
+
52
+ position: relative;
53
+ display: inline-block;
54
+ padding: 1rem 1.25rem;
55
+ background-color: var(--bubble-bg);
56
+ border: 1px solid var(--bubble-border);
57
+ border-radius: 1rem;
58
+ max-width: 100%;
59
+ margin-bottom: var(--space-sm);
60
+ }
61
+
62
+ .bubble-content {
63
+ font-size: var(--font-size-base);
64
+ line-height: var(--line-height-body);
65
+ }
66
+
67
+ .bubble-content p:first-child { margin-top: 0; }
68
+ .bubble-content p:last-child { margin-bottom: 0; }
69
+
70
+ /* Speech tail: ::before = border, ::after = fill (same size, 1px offset) */
71
+ .bubble-speech.bubble-bottom::after {
72
+ content: '';
73
+ position: absolute;
74
+ bottom: -12px;
75
+ left: 23px;
76
+ border-width: 13px 9px 0 9px;
77
+ border-style: solid;
78
+ border-color: var(--bubble-bg) transparent transparent transparent;
79
+ }
80
+
81
+ .bubble-speech.bubble-bottom::before {
82
+ content: '';
83
+ position: absolute;
84
+ bottom: -13px;
85
+ left: 23px;
86
+ border-width: 13px 9px 0 9px;
87
+ border-style: solid;
88
+ border-color: var(--bubble-border) transparent transparent transparent;
89
+ }
90
+
91
+ .bubble-speech.bubble-top::after {
92
+ content: '';
93
+ position: absolute;
94
+ top: -12px;
95
+ left: 23px;
96
+ border-width: 0 9px 13px 9px;
97
+ border-style: solid;
98
+ border-color: transparent transparent var(--bubble-bg) transparent;
99
+ }
100
+
101
+ .bubble-speech.bubble-top::before {
102
+ content: '';
103
+ position: absolute;
104
+ top: -13px;
105
+ left: 23px;
106
+ border-width: 0 9px 13px 9px;
107
+ border-style: solid;
108
+ border-color: transparent transparent var(--bubble-border) transparent;
109
+ }
110
+
111
+ .bubble-speech.bubble-left::after {
112
+ content: '';
113
+ position: absolute;
114
+ left: -12px;
115
+ top: 15px;
116
+ border-width: 9px 13px 9px 0;
117
+ border-style: solid;
118
+ border-color: transparent var(--bubble-bg) transparent transparent;
119
+ }
120
+
121
+ .bubble-speech.bubble-left::before {
122
+ content: '';
123
+ position: absolute;
124
+ left: -13px;
125
+ top: 15px;
126
+ border-width: 9px 13px 9px 0;
127
+ border-style: solid;
128
+ border-color: transparent var(--bubble-border) transparent transparent;
129
+ }
130
+
131
+ .bubble-speech.bubble-right::after {
132
+ content: '';
133
+ position: absolute;
134
+ right: -12px;
135
+ top: 15px;
136
+ border-width: 9px 0 9px 13px;
137
+ border-style: solid;
138
+ border-color: transparent transparent transparent var(--bubble-bg);
139
+ }
140
+
141
+ .bubble-speech.bubble-right::before {
142
+ content: '';
143
+ position: absolute;
144
+ right: -13px;
145
+ top: 15px;
146
+ border-width: 9px 0 9px 13px;
147
+ border-style: solid;
148
+ border-color: transparent transparent transparent var(--bubble-border);
149
+ }
150
+
151
+ /* Thought bubble (circles) */
152
+ .bubble-thought::after,
153
+ .bubble-thought::before {
154
+ content: '';
155
+ position: absolute;
156
+ background-color: var(--bubble-bg);
157
+ border: 1px solid var(--bubble-border);
158
+ border-radius: 50%;
159
+ }
160
+
161
+ .bubble-thought.bubble-bottom::after { width: 16px; height: 16px; bottom: -10px; left: 32px; }
162
+ .bubble-thought.bubble-bottom::before { width: 10px; height: 10px; bottom: -20px; left: 24px; }
163
+ .bubble-thought.bubble-top::after { width: 16px; height: 16px; top: -10px; left: 32px; }
164
+ .bubble-thought.bubble-top::before { width: 10px; height: 10px; top: -20px; left: 24px; }
165
+ .bubble-thought.bubble-left::after { width: 16px; height: 16px; left: -10px; top: 20px; }
166
+ .bubble-thought.bubble-left::before { width: 10px; height: 10px; left: -22px; top: 16px; }
167
+ .bubble-thought.bubble-right::after { width: 16px; height: 16px; right: -10px; top: 20px; }
168
+ .bubble-thought.bubble-right::before { width: 10px; height: 10px; right: -22px; top: 16px; }
169
+ </style>
@@ -0,0 +1,542 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Stepper component - unified step/progress/timeline visualization
4
+ *
5
+ * Replaces Timeline, TimelineItem, Steps, ProcessFlow.
6
+ *
7
+ * <Stepper :items="['Plan', 'Build', 'Test']" variant="numbers" />
8
+ * <Stepper :items="items" direction="vertical" variant="dots" mode="clicks" />
9
+ * <Stepper :items="['A', 'B', 'C']" mode="clicks">
10
+ * <template #step1>Panel A</template>
11
+ * <template #step2>Panel B</template>
12
+ * </Stepper>
13
+ */
14
+
15
+ import type { ClicksContext } from '@slidev/types'
16
+ import { computed, inject, ref, useSlots, onMounted, onUnmounted, watchEffect, type Ref } from 'vue'
17
+ import type { SemanticColor } from '../composables/useColors'
18
+ import { semanticColorVar, semanticForegroundVar } from '../composables/useColors'
19
+
20
+ interface StepperItem {
21
+ label: string
22
+ date?: string
23
+ description?: string
24
+ }
25
+
26
+ type VAlign = 'top' | 'center' | 'bottom'
27
+
28
+ const props = withDefaults(defineProps<{
29
+ items: string[] | StepperItem[]
30
+ variant?: 'dots' | 'numbers' | 'arrows'
31
+ direction?: 'horizontal' | 'vertical'
32
+ mode?: 'static' | 'clicks'
33
+ current?: number
34
+ color?: SemanticColor
35
+ trackValign?: VAlign
36
+ contentValign?: VAlign
37
+ }>(), {
38
+ variant: 'numbers',
39
+ direction: 'horizontal',
40
+ mode: 'static',
41
+ color: 'primary',
42
+ trackValign: 'center',
43
+ contentValign: 'center',
44
+ })
45
+
46
+ const slots = useSlots()
47
+
48
+ // Normalize items to StepperItem[]
49
+ const normalizedItems = computed<StepperItem[]>(() =>
50
+ props.items.map(item =>
51
+ typeof item === 'string' ? { label: item } : item,
52
+ ),
53
+ )
54
+
55
+ // Slidev clicks context
56
+ const defaultClicksContext: ClicksContext = {
57
+ current: 0,
58
+ clicksStart: 0,
59
+ total: 0,
60
+ isMounted: false,
61
+ currentOffset: 0,
62
+ relativeSizeMap: new Map(),
63
+ maxMap: new Map(),
64
+ setup: () => {},
65
+ calculateSince: () => null,
66
+ calculateRange: () => null,
67
+ calculate: () => null,
68
+ register: () => {},
69
+ unregister: () => {},
70
+ }
71
+ const clicksContext = inject<Ref<ClicksContext>>('$$slidev-clicks-context', ref(defaultClicksContext))
72
+
73
+ // --- Auto v-click registration (clicks mode) ---
74
+ const rootEl = ref<HTMLElement | null>(null)
75
+ const autoActiveStep = ref(1)
76
+ const isAutoRegistered = ref(false)
77
+
78
+ onMounted(() => {
79
+ if (props.mode !== 'clicks') return
80
+ const ctx = clicksContext.value
81
+ if (!ctx?.calculateSince || !rootEl.value) return
82
+
83
+ const size = normalizedItems.value.length - 1
84
+ if (size <= 0) return
85
+
86
+ const info = ctx.calculateSince('+1', size)
87
+ if (!info) return
88
+
89
+ ctx.register(rootEl.value, info)
90
+ isAutoRegistered.value = true
91
+
92
+ const isActiveRef = info.isActive
93
+ const offsetRef = info.currentOffset
94
+
95
+ watchEffect(() => {
96
+ const active = isActiveRef.value
97
+ const offset = offsetRef.value
98
+ autoActiveStep.value = active ? Math.min(offset + 2, normalizedItems.value.length) : 1
99
+ })
100
+ })
101
+
102
+ onUnmounted(() => {
103
+ if (isAutoRegistered.value && clicksContext.value?.unregister && rootEl.value) {
104
+ clicksContext.value.unregister(rootEl.value)
105
+ }
106
+ })
107
+
108
+ // Active step (1-indexed)
109
+ const activeStep = computed(() => {
110
+ if (props.mode === 'clicks') {
111
+ if (isAutoRegistered.value) return autoActiveStep.value
112
+ // Fallback: direct click count (non-Slidev contexts)
113
+ const clicks = clicksContext.value?.current ?? 0
114
+ return Math.min(clicks + 1, normalizedItems.value.length)
115
+ }
116
+ return props.current
117
+ })
118
+
119
+ // Content slots
120
+ const hasContent = computed(() =>
121
+ normalizedItems.value.some((_, i) => slots[`step${i + 1}`]),
122
+ )
123
+
124
+ // Marker size CSS var per variant
125
+ const markerSize = computed(() => {
126
+ switch (props.variant) {
127
+ case 'dots': return '0.75rem'
128
+ case 'numbers': return '2rem'
129
+ case 'arrows': return '3rem'
130
+ }
131
+ })
132
+
133
+ // Vertical alignment mapping
134
+ const valignMap: Record<VAlign, string> = {
135
+ top: 'flex-start',
136
+ center: 'center',
137
+ bottom: 'flex-end',
138
+ }
139
+
140
+ // CSS custom properties
141
+ const cssVars = computed(() => ({
142
+ '--stepper-color': semanticColorVar[props.color],
143
+ '--stepper-foreground': semanticForegroundVar[props.color],
144
+ '--stepper-marker-size': markerSize.value,
145
+ '--stepper-track-valign': valignMap[props.trackValign],
146
+ '--stepper-content-valign': valignMap[props.contentValign],
147
+ }))
148
+
149
+ // Step status helpers
150
+ function isCompleted(index: number) {
151
+ return activeStep.value !== undefined && index + 1 < activeStep.value
152
+ }
153
+
154
+ function isActive(index: number) {
155
+ return index + 1 === activeStep.value
156
+ }
157
+
158
+ function isUpcoming(index: number) {
159
+ if (props.mode !== 'clicks') return false
160
+ return activeStep.value !== undefined && index + 1 > activeStep.value
161
+ }
162
+ </script>
163
+
164
+ <template>
165
+ <div
166
+ ref="rootEl"
167
+ class="stepper"
168
+ :class="[
169
+ `stepper-${variant}`,
170
+ `stepper-${direction}`,
171
+ ]"
172
+ :style="cssVars"
173
+ >
174
+ <!-- Track -->
175
+ <div class="stepper-track">
176
+ <template v-for="(item, index) in normalizedItems" :key="index">
177
+ <div
178
+ class="stepper-step"
179
+ :class="{
180
+ 'is-active': isActive(index),
181
+ 'is-completed': isCompleted(index),
182
+ 'is-upcoming': isUpcoming(index),
183
+ }"
184
+ >
185
+ <!-- Marker -->
186
+ <div class="stepper-marker">
187
+ <!-- Completed check (numbers + arrows only) -->
188
+ <template v-if="isCompleted(index) && variant !== 'dots'">
189
+ <svg class="stepper-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">
190
+ <polyline points="20 6 9 17 4 12" />
191
+ </svg>
192
+ </template>
193
+ <!-- Number (numbers + arrows) -->
194
+ <template v-else-if="variant !== 'dots'">
195
+ {{ index + 1 }}
196
+ </template>
197
+ <!-- Dots: empty circle, no content -->
198
+ </div>
199
+
200
+ <!-- Label + description (inside step for layout) -->
201
+ <div class="stepper-label-group">
202
+ <span v-if="item.date" class="stepper-date">{{ item.date }}</span>
203
+ <span class="stepper-label">{{ item.label }}</span>
204
+ <span v-if="item.description" class="stepper-description">{{ item.description }}</span>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- Connector (not on last) -->
209
+ <div
210
+ v-if="index < normalizedItems.length - 1"
211
+ class="stepper-connector"
212
+ :class="{ 'is-completed': isCompleted(index) }"
213
+ >
214
+ <span v-if="variant === 'arrows'" class="stepper-arrow-char">
215
+ {{ direction === 'horizontal' ? '\u2192' : '\u2193' }}
216
+ </span>
217
+ </div>
218
+ </template>
219
+ </div>
220
+
221
+ <!-- Content area (clicks mode with slots) -->
222
+ <div v-if="hasContent && mode === 'clicks'" class="stepper-content">
223
+ <Transition name="stepper-panel" mode="out-in">
224
+ <div
225
+ v-if="activeStep !== undefined"
226
+ :key="activeStep"
227
+ class="stepper-panel"
228
+ >
229
+ <slot :name="`step${activeStep}`" />
230
+ </div>
231
+ </Transition>
232
+ </div>
233
+ </div>
234
+ </template>
235
+
236
+ <style scoped>
237
+ .stepper {
238
+ display: flex;
239
+ flex-direction: column;
240
+ width: 100%;
241
+ padding: 4px;
242
+ }
243
+
244
+ /* ===== Track ===== */
245
+
246
+ .stepper-track {
247
+ display: flex;
248
+ align-items: flex-start;
249
+ }
250
+
251
+ .stepper-horizontal .stepper-track {
252
+ flex-direction: row;
253
+ align-items: flex-start;
254
+ justify-content: center;
255
+ gap: 0.5rem;
256
+ }
257
+
258
+ .stepper-vertical .stepper-track {
259
+ flex-direction: column;
260
+ gap: 0;
261
+ }
262
+
263
+ /* ===== Step ===== */
264
+
265
+ .stepper-step {
266
+ display: flex;
267
+ align-items: center;
268
+ }
269
+
270
+ .stepper-horizontal .stepper-step {
271
+ flex-direction: column;
272
+ align-items: center;
273
+ text-align: center;
274
+ }
275
+
276
+ .stepper-horizontal.stepper-arrows .stepper-step {
277
+ flex: 1;
278
+ }
279
+
280
+ .stepper-vertical .stepper-step {
281
+ flex-direction: row;
282
+ gap: var(--space-sm);
283
+ }
284
+
285
+ .stepper-step.is-upcoming {
286
+ opacity: 0.4;
287
+ }
288
+
289
+ /* ===== Marker ===== */
290
+
291
+ .stepper-marker {
292
+ width: var(--stepper-marker-size);
293
+ height: var(--stepper-marker-size);
294
+ border-radius: 50%;
295
+ display: flex;
296
+ align-items: center;
297
+ justify-content: center;
298
+ flex-shrink: 0;
299
+ transition:
300
+ background-color var(--dur-moderate-02, 240ms) var(--ease-standard, ease),
301
+ color var(--dur-moderate-02, 240ms) var(--ease-standard, ease),
302
+ transform var(--dur-moderate-01, 150ms) var(--ease-expressive, ease);
303
+ }
304
+
305
+ /* -- Dots variant -- */
306
+
307
+ .stepper-dots .stepper-marker {
308
+ background-color: var(--color-bg-muted);
309
+ }
310
+
311
+ .stepper-dots .stepper-step.is-active .stepper-marker {
312
+ background-color: var(--stepper-color);
313
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--stepper-color) 30%, transparent);
314
+ }
315
+
316
+ .stepper-dots .stepper-step.is-completed .stepper-marker {
317
+ background-color: var(--color-success);
318
+ }
319
+
320
+ /* -- Numbers variant -- */
321
+
322
+ .stepper-numbers .stepper-marker {
323
+ background-color: var(--color-bg-muted);
324
+ color: var(--color-text);
325
+ font-size: var(--font-size-small);
326
+ font-weight: var(--font-weight-bold);
327
+ }
328
+
329
+ .stepper-numbers .stepper-step.is-active .stepper-marker {
330
+ background-color: var(--stepper-color);
331
+ color: var(--stepper-foreground);
332
+ transform: scale(1.15);
333
+ }
334
+
335
+ .stepper-numbers .stepper-step.is-completed .stepper-marker {
336
+ background-color: var(--color-success);
337
+ color: var(--color-success-foreground);
338
+ }
339
+
340
+ /* -- Arrows variant -- */
341
+
342
+ .stepper-arrows .stepper-marker {
343
+ background-color: var(--color-bg-muted);
344
+ color: var(--color-text);
345
+ font-size: var(--font-size-small);
346
+ font-weight: var(--font-weight-bold);
347
+ }
348
+
349
+ .stepper-arrows .stepper-step.is-active .stepper-marker {
350
+ background-color: var(--stepper-color);
351
+ color: var(--stepper-foreground);
352
+ transform: scale(1.1);
353
+ }
354
+
355
+ .stepper-arrows .stepper-step.is-completed .stepper-marker {
356
+ background-color: var(--color-success);
357
+ color: var(--color-success-foreground);
358
+ }
359
+
360
+ /* ===== Check SVG ===== */
361
+
362
+ .stepper-check {
363
+ width: 1rem;
364
+ height: 1rem;
365
+ }
366
+
367
+ /* ===== Labels ===== */
368
+
369
+ .stepper-label-group {
370
+ display: flex;
371
+ flex-direction: column;
372
+ }
373
+
374
+ .stepper-horizontal .stepper-label-group {
375
+ align-items: center;
376
+ margin-top: 0.5rem;
377
+ }
378
+
379
+ .stepper-vertical .stepper-label-group {
380
+ padding-bottom: 0;
381
+ }
382
+
383
+ .stepper-date {
384
+ font-size: var(--font-size-small);
385
+ color: var(--stepper-color);
386
+ font-weight: var(--font-weight-medium);
387
+ margin-bottom: 0.125rem;
388
+ }
389
+
390
+ .stepper-label {
391
+ font-size: var(--font-size-small);
392
+ font-weight: var(--font-weight-semibold);
393
+ }
394
+
395
+ .stepper-description {
396
+ font-size: var(--font-size-small);
397
+ color: var(--color-text-secondary);
398
+ line-height: var(--line-height-body);
399
+ margin-top: 0.125rem;
400
+ }
401
+
402
+ /* ===== Connectors ===== */
403
+
404
+ .stepper-connector {
405
+ position: relative;
406
+ }
407
+
408
+ /* Horizontal connectors */
409
+
410
+ .stepper-horizontal .stepper-connector {
411
+ height: 2px;
412
+ flex: 0 0 2rem;
413
+ background-color: var(--color-border);
414
+ overflow: hidden;
415
+ margin-top: calc(var(--stepper-marker-size) / 2 - 1px);
416
+ }
417
+
418
+ /* dots: same margin-top formula from parent rule applies */
419
+
420
+ .stepper-horizontal .stepper-connector::after {
421
+ content: '';
422
+ position: absolute;
423
+ top: 0;
424
+ left: 0;
425
+ height: 100%;
426
+ width: 0%;
427
+ background-color: var(--color-success);
428
+ transition: width var(--dur-moderate-02, 240ms) var(--ease-standard, ease);
429
+ }
430
+
431
+ .stepper-horizontal .stepper-connector.is-completed::after {
432
+ width: 100%;
433
+ }
434
+
435
+ /* Arrows variant: no line, show arrow character */
436
+
437
+ .stepper-horizontal.stepper-arrows .stepper-connector {
438
+ height: auto;
439
+ background: none;
440
+ display: flex;
441
+ align-items: center;
442
+ justify-content: center;
443
+ flex: 0 0 auto;
444
+ margin-top: calc(var(--stepper-marker-size) / 2 - 1rem);
445
+ }
446
+
447
+ .stepper-horizontal.stepper-arrows .stepper-connector::after {
448
+ display: none;
449
+ }
450
+
451
+ .stepper-arrow-char {
452
+ color: var(--color-text-tertiary);
453
+ font-size: 2rem;
454
+ line-height: 1;
455
+ }
456
+
457
+ .stepper-connector.is-completed .stepper-arrow-char {
458
+ color: var(--color-success);
459
+ }
460
+
461
+ /* Vertical connectors */
462
+
463
+ .stepper-vertical .stepper-connector {
464
+ width: 2px;
465
+ min-height: 1rem;
466
+ flex: 0 0 auto;
467
+ background-color: var(--color-border);
468
+ margin-left: calc(var(--stepper-marker-size) / 2 - 1px);
469
+ margin-top: 0.25rem;
470
+ margin-bottom: 0.25rem;
471
+ }
472
+
473
+ .stepper-vertical.stepper-arrows .stepper-connector {
474
+ background: none;
475
+ display: flex;
476
+ align-items: center;
477
+ margin-left: calc(var(--stepper-marker-size) / 2 - 0.5rem);
478
+ }
479
+
480
+ .stepper-vertical .stepper-connector.is-completed {
481
+ background-color: var(--color-success);
482
+ }
483
+
484
+ .stepper-vertical.stepper-arrows .stepper-connector.is-completed {
485
+ background: none;
486
+ }
487
+
488
+ /* ===== Content area ===== */
489
+
490
+ .stepper-content {
491
+ margin-top: var(--space-md);
492
+ min-height: 4rem;
493
+ }
494
+
495
+ /* Vertical + content: side-by-side layout (track left, content right) */
496
+ .stepper-vertical:has(.stepper-content) {
497
+ flex-direction: row;
498
+ gap: var(--space-lg);
499
+ flex: 1;
500
+ }
501
+
502
+ .stepper-vertical:has(.stepper-content) .stepper-track {
503
+ flex-shrink: 0;
504
+ align-self: var(--stepper-track-valign, center);
505
+ }
506
+
507
+ .stepper-vertical:has(.stepper-content) .stepper-content {
508
+ flex: 1;
509
+ margin-top: 0;
510
+ min-height: 0;
511
+ align-self: var(--stepper-content-valign, center);
512
+ }
513
+
514
+ .stepper-panel :deep(:first-child) {
515
+ margin-block-start: 0;
516
+ }
517
+ </style>
518
+
519
+ <!-- Transition classes must be unscoped (Vue requirement) -->
520
+ <style>
521
+ .stepper-panel-enter-active {
522
+ transition:
523
+ opacity var(--dur-moderate-02, 240ms) var(--ease-enter, ease-out),
524
+ transform var(--dur-moderate-02, 240ms) var(--ease-enter, ease-out);
525
+ }
526
+
527
+ .stepper-panel-leave-active {
528
+ transition:
529
+ opacity var(--dur-fast-02, 110ms) var(--ease-exit, ease-in),
530
+ transform var(--dur-fast-02, 110ms) var(--ease-exit, ease-in);
531
+ }
532
+
533
+ .stepper-panel-enter-from {
534
+ opacity: 0;
535
+ transform: translateY(var(--shift-sm, 12px));
536
+ }
537
+
538
+ .stepper-panel-leave-to {
539
+ opacity: 0;
540
+ transform: translateY(calc(var(--shift-xs, 8px) * -1));
541
+ }
542
+ </style>