@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,392 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * DeviceMockup component - device frames for screenshots
4
+ *
5
+ * Usage:
6
+ * <DeviceMockup device="iphone" image="/screenshot.png" />
7
+ * <DeviceMockup device="browser" url="https://example.com">
8
+ * <img src="/screenshot.png" />
9
+ * </DeviceMockup>
10
+ * <DeviceMockup device="macbook" :scale="0.8" />
11
+ */
12
+
13
+ import { computed } from 'vue'
14
+
15
+ const props = defineProps<{
16
+ device: 'iphone' | 'macbook' | 'browser' | 'tablet'
17
+ image?: string
18
+ url?: string
19
+ title?: string
20
+ scale?: number
21
+ placeholder?: string // Renders a styled placeholder with text
22
+ }>()
23
+
24
+ const deviceClass = computed(() => {
25
+ return `device-mockup device-${props.device}`
26
+ })
27
+
28
+ const containerStyle = computed(() => {
29
+ const styles: Record<string, string> = {}
30
+ if (props.scale && props.scale !== 1) {
31
+ styles.zoom = `${props.scale}`
32
+ }
33
+ return styles
34
+ })
35
+
36
+ const displayUrl = computed(() => {
37
+ if (!props.url) return ''
38
+ try {
39
+ const url = new URL(props.url)
40
+ return url.hostname + (url.pathname !== '/' ? url.pathname : '')
41
+ } catch {
42
+ return props.url
43
+ }
44
+ })
45
+ </script>
46
+
47
+ <template>
48
+ <div :class="deviceClass" :style="containerStyle">
49
+ <!-- iPhone -->
50
+ <template v-if="device === 'iphone'">
51
+ <div class="iphone-frame">
52
+ <div class="iphone-notch"></div>
53
+ <div class="iphone-screen">
54
+ <img v-if="image" :src="image" :alt="title || 'iPhone screenshot'" />
55
+ <div v-else-if="placeholder" class="device-mockup-placeholder">
56
+ <p>{{ placeholder }}</p>
57
+ </div>
58
+ <slot v-else />
59
+ </div>
60
+ </div>
61
+ </template>
62
+
63
+ <!-- MacBook -->
64
+ <template v-else-if="device === 'macbook'">
65
+ <div class="macbook-frame">
66
+ <div class="macbook-screen">
67
+ <div class="macbook-camera"></div>
68
+ <div class="macbook-display">
69
+ <img v-if="image" :src="image" :alt="title || 'MacBook screenshot'" />
70
+ <div v-else-if="placeholder" class="device-mockup-placeholder">
71
+ <p>{{ placeholder }}</p>
72
+ </div>
73
+ <slot v-else />
74
+ </div>
75
+ </div>
76
+ <div class="macbook-base">
77
+ <div class="macbook-notch"></div>
78
+ </div>
79
+ </div>
80
+ </template>
81
+
82
+ <!-- Browser -->
83
+ <template v-else-if="device === 'browser'">
84
+ <div class="browser-frame">
85
+ <div class="browser-toolbar">
86
+ <div class="browser-buttons">
87
+ <span class="browser-btn browser-btn-close"></span>
88
+ <span class="browser-btn browser-btn-minimize"></span>
89
+ <span class="browser-btn browser-btn-maximize"></span>
90
+ </div>
91
+ <div class="browser-address">
92
+ <svg class="browser-lock" viewBox="0 0 16 16" fill="currentColor">
93
+ <path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V6H4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-.5V4.5A3.5 3.5 0 0 0 8 1zm2.5 5V4.5a2.5 2.5 0 0 0-5 0V6h5z"/>
94
+ </svg>
95
+ <span class="browser-url">{{ displayUrl || 'example.com' }}</span>
96
+ </div>
97
+ </div>
98
+ <div class="browser-content" :class="{ 'browser-content-image': !!image }">
99
+ <img v-if="image" :src="image" :alt="title || 'Browser screenshot'" />
100
+ <div v-else-if="placeholder" class="device-mockup-placeholder">
101
+ <p>{{ placeholder }}</p>
102
+ </div>
103
+ <slot v-else />
104
+ </div>
105
+ </div>
106
+ </template>
107
+
108
+ <!-- Tablet -->
109
+ <template v-else-if="device === 'tablet'">
110
+ <div class="tablet-frame">
111
+ <div class="tablet-camera"></div>
112
+ <div class="tablet-screen">
113
+ <img v-if="image" :src="image" :alt="title || 'Tablet screenshot'" />
114
+ <div v-else-if="placeholder" class="device-mockup-placeholder">
115
+ <p>{{ placeholder }}</p>
116
+ </div>
117
+ <slot v-else />
118
+ </div>
119
+ </div>
120
+ </template>
121
+ </div>
122
+ </template>
123
+
124
+ <style>
125
+ .device-mockup {
126
+ display: inline-block;
127
+ }
128
+
129
+ /* ============================================
130
+ iPhone
131
+ ============================================ */
132
+ .iphone-frame {
133
+ position: relative;
134
+ width: 280px;
135
+ height: 580px;
136
+ background-color: var(--color-drac-fg-900);
137
+ border-radius: 40px;
138
+ padding: 12px;
139
+ box-shadow:
140
+ 0 0 0 2px var(--color-drac-fg-700),
141
+ 0 20px 40px color-mix(in srgb, var(--color-black) 30%, transparent);
142
+ }
143
+
144
+ .dark .iphone-frame {
145
+ background-color: var(--color-drac-fg-800);
146
+ box-shadow:
147
+ 0 0 0 2px var(--color-drac-fg-600),
148
+ 0 20px 40px color-mix(in srgb, var(--color-black) 50%, transparent);
149
+ }
150
+
151
+ .iphone-notch {
152
+ position: absolute;
153
+ top: 12px;
154
+ left: 50%;
155
+ transform: translateX(-50%);
156
+ width: 120px;
157
+ height: 28px;
158
+ background-color: var(--color-drac-fg-900);
159
+ border-radius: 0 0 16px 16px;
160
+ z-index: 10;
161
+ }
162
+
163
+ .dark .iphone-notch {
164
+ background-color: var(--color-drac-fg-800);
165
+ }
166
+
167
+ .iphone-screen {
168
+ width: 100%;
169
+ height: 100%;
170
+ background-color: var(--color-bg);
171
+ border-radius: 28px;
172
+ overflow: hidden;
173
+ }
174
+
175
+ .iphone-screen img {
176
+ width: 100%;
177
+ height: 100%;
178
+ object-fit: cover;
179
+ }
180
+
181
+ /* ============================================
182
+ MacBook
183
+ ============================================ */
184
+ .macbook-frame {
185
+ display: flex;
186
+ flex-direction: column;
187
+ align-items: center;
188
+ }
189
+
190
+ .macbook-screen {
191
+ position: relative;
192
+ width: 560px;
193
+ background-color: var(--color-drac-fg-900);
194
+ border-radius: 12px 12px 0 0;
195
+ padding: 8px 8px 0 8px;
196
+ box-shadow: 0 -2px 20px color-mix(in srgb, var(--color-black) 20%, transparent);
197
+ }
198
+
199
+ .dark .macbook-screen {
200
+ background-color: var(--color-drac-fg-800);
201
+ }
202
+
203
+ .macbook-camera {
204
+ width: 8px;
205
+ height: 8px;
206
+ background-color: var(--color-drac-fg-700);
207
+ border-radius: 50%;
208
+ margin: 4px auto 8px auto;
209
+ }
210
+
211
+ .dark .macbook-camera {
212
+ background-color: var(--color-drac-fg-600);
213
+ }
214
+
215
+ .macbook-display {
216
+ width: 100%;
217
+ aspect-ratio: 16 / 10;
218
+ background-color: var(--color-bg);
219
+ border-radius: 4px 4px 0 0;
220
+ overflow: hidden;
221
+ }
222
+
223
+ .macbook-display img {
224
+ width: 100%;
225
+ height: 100%;
226
+ object-fit: cover;
227
+ }
228
+
229
+ .macbook-base {
230
+ width: 620px;
231
+ height: 14px;
232
+ background: linear-gradient(to bottom, var(--color-drac-fg-300) 0%, var(--color-drac-fg-400) 100%);
233
+ border-radius: 0 0 8px 8px;
234
+ display: flex;
235
+ justify-content: center;
236
+ align-items: flex-start;
237
+ }
238
+
239
+ .dark .macbook-base {
240
+ background: linear-gradient(to bottom, var(--color-drac-fg-600) 0%, var(--color-drac-fg-700) 100%);
241
+ }
242
+
243
+ .macbook-notch {
244
+ width: 80px;
245
+ height: 4px;
246
+ background-color: var(--color-drac-fg-500);
247
+ border-radius: 0 0 4px 4px;
248
+ }
249
+
250
+ .dark .macbook-notch {
251
+ background-color: var(--color-drac-fg-500);
252
+ }
253
+
254
+ /* ============================================
255
+ Browser
256
+ ============================================ */
257
+ .browser-frame {
258
+ width: 100%;
259
+ max-width: 640px;
260
+ background-color: var(--color-bg-soft);
261
+ border: 1px solid var(--color-border);
262
+ border-radius: 8px;
263
+ overflow: hidden;
264
+ box-shadow: 0 4px 20px color-mix(in srgb, var(--color-black) 10%, transparent);
265
+ }
266
+
267
+ .dark .browser-frame {
268
+ box-shadow: 0 4px 20px color-mix(in srgb, var(--color-black) 30%, transparent);
269
+ }
270
+
271
+ .browser-toolbar {
272
+ display: flex;
273
+ align-items: center;
274
+ gap: 12px;
275
+ padding: 10px 12px;
276
+ background-color: var(--color-bg-muted);
277
+ border-bottom: 1px solid var(--color-border);
278
+ }
279
+
280
+ .browser-buttons {
281
+ display: flex;
282
+ gap: 6px;
283
+ }
284
+
285
+ .browser-btn {
286
+ width: 12px;
287
+ height: 12px;
288
+ border-radius: 50%;
289
+ }
290
+
291
+ .browser-btn-close {
292
+ background-color: var(--color-drac-red-500);
293
+ }
294
+
295
+ .browser-btn-minimize {
296
+ background-color: var(--color-drac-orange-400);
297
+ }
298
+
299
+ .browser-btn-maximize {
300
+ background-color: var(--color-drac-green-500);
301
+ }
302
+
303
+ .browser-address {
304
+ flex: 1;
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 6px;
308
+ padding: 6px 12px;
309
+ background-color: var(--color-bg);
310
+ border-radius: 6px;
311
+ font-size: 0.75rem;
312
+ color: var(--color-text-secondary);
313
+ }
314
+
315
+ .browser-lock {
316
+ width: 12px;
317
+ height: 12px;
318
+ opacity: 0.6;
319
+ }
320
+
321
+ .browser-url {
322
+ overflow: hidden;
323
+ text-overflow: ellipsis;
324
+ white-space: nowrap;
325
+ }
326
+
327
+ .browser-content {
328
+ background-color: var(--color-bg);
329
+ overflow: hidden;
330
+ }
331
+
332
+ .browser-content-image {
333
+ aspect-ratio: 16 / 9;
334
+ }
335
+
336
+ .browser-content img {
337
+ width: 100%;
338
+ height: 100%;
339
+ object-fit: cover;
340
+ }
341
+
342
+ /* ============================================
343
+ Tablet (landscape)
344
+ ============================================ */
345
+ .tablet-frame {
346
+ position: relative;
347
+ width: 500px;
348
+ height: 360px;
349
+ background-color: var(--color-drac-fg-900);
350
+ border-radius: 20px;
351
+ padding: 16px;
352
+ box-shadow:
353
+ 0 0 0 2px var(--color-drac-fg-700),
354
+ 0 20px 40px color-mix(in srgb, var(--color-black) 30%, transparent);
355
+ }
356
+
357
+ .dark .tablet-frame {
358
+ background-color: var(--color-drac-fg-800);
359
+ box-shadow:
360
+ 0 0 0 2px var(--color-drac-fg-600),
361
+ 0 20px 40px color-mix(in srgb, var(--color-black) 50%, transparent);
362
+ }
363
+
364
+ .tablet-camera {
365
+ position: absolute;
366
+ left: 50%;
367
+ top: 6px;
368
+ transform: translateX(-50%);
369
+ width: 8px;
370
+ height: 8px;
371
+ background-color: var(--color-drac-fg-700);
372
+ border-radius: 50%;
373
+ }
374
+
375
+ .dark .tablet-camera {
376
+ background-color: var(--color-drac-fg-600);
377
+ }
378
+
379
+ .tablet-screen {
380
+ width: 100%;
381
+ height: 100%;
382
+ background-color: var(--color-bg);
383
+ border-radius: 4px;
384
+ overflow: hidden;
385
+ }
386
+
387
+ .tablet-screen img {
388
+ width: 100%;
389
+ height: 100%;
390
+ object-fit: cover;
391
+ }
392
+ </style>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Funnel widget — conversion funnel visualization.
4
+ *
5
+ * Displays stages as vertically stacked bars with decreasing widths,
6
+ * forming a classic funnel/trapezoid shape.
7
+ *
8
+ * <Funnel :stages="[
9
+ * { label: 'Visitors', value: '10,000' },
10
+ * { label: 'Leads', value: '2,500' },
11
+ * { label: 'Customers', value: '100' }
12
+ * ]" />
13
+ */
14
+
15
+ import type { SemanticColor } from '../composables/useColors'
16
+ import { semanticColorVar, semanticForegroundVar } from '../composables/useColors'
17
+
18
+ const props = withDefaults(defineProps<{
19
+ stages: Array<{ label: string; value: string }>
20
+ colors?: string[]
21
+ }>(), {
22
+ colors: () => ['primary', 'info', 'success'],
23
+ })
24
+
25
+ function stageColor(index: number): string {
26
+ const c = props.colors[index % props.colors.length] as SemanticColor
27
+ return semanticColorVar[c] || semanticColorVar.primary
28
+ }
29
+
30
+ function stageForeground(index: number): string {
31
+ const c = props.colors[index % props.colors.length] as SemanticColor
32
+ return semanticForegroundVar[c] || semanticForegroundVar.primary
33
+ }
34
+
35
+ function stageWidth(index: number): number {
36
+ const total = props.stages.length
37
+ if (total <= 1) return 100
38
+ const minRatio = 0.35
39
+ return 100 * (1 - index * (1 - minRatio) / (total - 1))
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <div class="funnel">
45
+ <div
46
+ v-for="(stage, i) in stages"
47
+ :key="i"
48
+ class="funnel-stage"
49
+ :style="{
50
+ width: stageWidth(i) + '%',
51
+ background: stageColor(i),
52
+ color: stageForeground(i),
53
+ }"
54
+ >
55
+ <span class="funnel-stage-value">{{ stage.value }}</span>
56
+ <span class="funnel-stage-label">{{ stage.label }}</span>
57
+ </div>
58
+ </div>
59
+ </template>
60
+
61
+ <style scoped>
62
+ .funnel {
63
+ display: flex;
64
+ flex-direction: column;
65
+ align-items: center;
66
+ gap: 0.25rem;
67
+ margin-top: var(--space-sm);
68
+ }
69
+
70
+ .funnel-stage {
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ gap: var(--space-sm);
75
+ border-radius: 0.5rem;
76
+ padding: 0.5rem var(--space-md);
77
+ }
78
+
79
+ .funnel-stage-value {
80
+ font-size: var(--font-size-base);
81
+ font-weight: var(--font-weight-bold);
82
+ }
83
+
84
+ .funnel-stage-label {
85
+ font-weight: var(--font-weight-semibold);
86
+ }
87
+ </style>
@@ -0,0 +1,73 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Icon primitive — Phosphor icon wrapper with theme-aware sizing and color.
4
+ *
5
+ * Renders SVG directly from @iconify-json/ph data via @iconify/utils.
6
+ * No runtime component resolution needed.
7
+ *
8
+ * Usage:
9
+ * <Icon name="ph-check-circle" size="xl" color="success" />
10
+ * <Icon name="ph-rocket-bold" size="lg" />
11
+ */
12
+
13
+ import { computed } from 'vue'
14
+ import { getIconData, iconToSVG } from '@iconify/utils'
15
+ import phIcons from '@iconify-json/ph/icons.json'
16
+ import type { ColorWithInherit } from '../composables/useColors'
17
+ import { textColorVar } from '../composables/useColors'
18
+
19
+ const props = defineProps<{
20
+ name: string
21
+ size?: 'sm' | 'md' | 'lg' | 'xl'
22
+ color?: ColorWithInherit
23
+ }>()
24
+
25
+ const sizeMap: Record<string, string> = {
26
+ sm: '1rem',
27
+ md: '1.5rem',
28
+ lg: '2rem',
29
+ xl: '3rem',
30
+ }
31
+
32
+ const iconSvg = computed(() => {
33
+ const iconName = props.name.replace(/^ph-/, '')
34
+ const data = getIconData(phIcons as any, iconName)
35
+ if (!data) return null
36
+ return iconToSVG(data)
37
+ })
38
+ </script>
39
+
40
+ <template>
41
+ <span
42
+ class="icon-wrapper"
43
+ :style="{
44
+ fontSize: sizeMap[size || 'md'],
45
+ color: color && color !== 'inherit' ? textColorVar[color] : undefined,
46
+ }"
47
+ >
48
+ <svg
49
+ v-if="iconSvg"
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ :width="iconSvg.attributes.width"
52
+ :height="iconSvg.attributes.height"
53
+ :viewBox="iconSvg.attributes.viewBox"
54
+ fill="currentColor"
55
+ v-html="iconSvg.body"
56
+ />
57
+ </span>
58
+ </template>
59
+
60
+ <style scoped>
61
+ .icon-wrapper {
62
+ display: inline-flex;
63
+ align-items: center;
64
+ justify-content: center;
65
+ vertical-align: middle;
66
+ line-height: 1;
67
+ }
68
+
69
+ .icon-wrapper svg {
70
+ width: 1em;
71
+ height: 1em;
72
+ }
73
+ </style>
@@ -0,0 +1,38 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Iframe primitive - embeds external content with proper sizing.
4
+ *
5
+ * In bg slots: auto-fills the area (parent CSS handles sizing).
6
+ * In content: specify height for proper display.
7
+ *
8
+ * Usage:
9
+ * ::bg::
10
+ * <Iframe src="https://sli.dev" />
11
+ *
12
+ * <Iframe src="https://codepen.io/..." height="400px" />
13
+ */
14
+
15
+ defineProps<{
16
+ src: string
17
+ height?: string
18
+ allow?: string
19
+ }>()
20
+ </script>
21
+
22
+ <template>
23
+ <iframe
24
+ :src="src"
25
+ :allow="allow"
26
+ :style="{ height: height || undefined }"
27
+ class="iframe-embed"
28
+ frameborder="0"
29
+ allowfullscreen
30
+ />
31
+ </template>
32
+
33
+ <style>
34
+ .iframe-embed {
35
+ width: 100%;
36
+ border: 0;
37
+ }
38
+ </style>
@@ -0,0 +1,69 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Image primitive — content image with caption, border, and sizing.
4
+ * Things markdown ![](url) can't express.
5
+ */
6
+
7
+ defineProps<{
8
+ src: string
9
+ alt?: string
10
+ caption?: string
11
+ fit?: 'cover' | 'contain' | 'none'
12
+ border?: boolean
13
+ shadow?: boolean
14
+ rounded?: boolean
15
+ width?: string
16
+ }>()
17
+ </script>
18
+
19
+ <template>
20
+ <figure class="image-figure" :style="width ? { width } : undefined">
21
+ <img
22
+ :src="src"
23
+ :alt="alt || ''"
24
+ class="image-img"
25
+ :class="{
26
+ 'image-border': border,
27
+ 'image-shadow': shadow,
28
+ 'image-rounded': rounded,
29
+ }"
30
+ :style="fit && fit !== 'none' ? { objectFit: fit, width: '100%', height: '100%' } : undefined"
31
+ />
32
+ <figcaption v-if="caption" class="image-caption">{{ caption }}</figcaption>
33
+ </figure>
34
+ </template>
35
+
36
+ <style scoped>
37
+ .image-figure {
38
+ margin: 0;
39
+ display: inline-flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ width: fit-content;
43
+ }
44
+
45
+ .image-img {
46
+ display: block;
47
+ max-width: 100%;
48
+ height: auto;
49
+ }
50
+
51
+ .image-border {
52
+ border: 1px solid var(--color-border);
53
+ }
54
+
55
+ .image-shadow {
56
+ box-shadow: 0 4px 12px color-mix(in srgb, var(--color-black) 15%, transparent);
57
+ }
58
+
59
+ .image-rounded {
60
+ border-radius: 0.5rem;
61
+ }
62
+
63
+ .image-caption {
64
+ font-size: var(--font-size-small);
65
+ color: var(--color-text-secondary);
66
+ margin-top: var(--space-xs);
67
+ text-align: center;
68
+ }
69
+ </style>