@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,476 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Countdown component - animated timer for breaks, exercises, demos
4
+ *
5
+ * Usage:
6
+ * <Countdown :duration="300" /> <!-- 5 minutes -->
7
+ * <Countdown :duration="60" autostart /> <!-- 1 minute, starts immediately -->
8
+ * <Countdown :duration="120" format="mm:ss" size="lg" />
9
+ * <Countdown :duration="30" showProgress color="danger" />
10
+ */
11
+
12
+ import { ref, unref, computed, watch, watchEffect, inject, onMounted, onUnmounted } from 'vue'
13
+ import type { Ref } from 'vue'
14
+
15
+ const defaultClicksContext = { current: 0 }
16
+ const defaultNav = { currentSlideNo: -1 }
17
+
18
+ import type { SemanticColor } from '../composables/useColors'
19
+
20
+ const props = withDefaults(defineProps<{
21
+ duration: number // seconds
22
+ autostart?: boolean
23
+ format?: 'mm:ss' | 'hh:mm:ss' | 'seconds'
24
+ size?: 'sm' | 'md' | 'lg' | 'xl'
25
+ color?: Extract<SemanticColor, 'primary' | 'danger' | 'warning' | 'success'>
26
+ showProgress?: boolean
27
+ at?: number // undefined = auto-register click, 0 = on slide enter, -1 = manual
28
+ }>(), {
29
+ autostart: false,
30
+ format: 'mm:ss',
31
+ size: 'lg',
32
+ color: 'primary',
33
+ showProgress: false,
34
+ })
35
+
36
+ const clicksContext = inject<Ref<{ current: number }>>('$$slidev-clicks-context', ref(defaultClicksContext))
37
+ const slidevContext = inject<{ nav: { currentSlideNo: number } }>('$$slidev-context', { nav: defaultNav })
38
+ const slidePage = inject<Ref<number>>('$$slidev-page', ref(-1))
39
+
40
+ const emit = defineEmits<{
41
+ (e: 'complete'): void
42
+ (e: 'tick', remaining: number): void
43
+ }>()
44
+
45
+ const rootEl = ref<HTMLElement | null>(null)
46
+ const remaining = ref(props.duration)
47
+ const isRunning = ref(false)
48
+ const isPaused = ref(false)
49
+ const isAutoRegistered = ref(false)
50
+ let intervalId: ReturnType<typeof setInterval> | null = null
51
+
52
+ const formattedTime = computed(() => {
53
+ const secs = remaining.value
54
+ if (props.format === 'seconds') {
55
+ return String(secs)
56
+ }
57
+
58
+ const hours = Math.floor(secs / 3600)
59
+ const minutes = Math.floor((secs % 3600) / 60)
60
+ const seconds = secs % 60
61
+
62
+ const pad = (n: number) => String(n).padStart(2, '0')
63
+
64
+ if (props.format === 'hh:mm:ss') {
65
+ return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
66
+ }
67
+ // mm:ss - if hours > 0, show them anyway
68
+ if (hours > 0) {
69
+ return `${hours}:${pad(minutes)}:${pad(seconds)}`
70
+ }
71
+ return `${pad(minutes)}:${pad(seconds)}`
72
+ })
73
+
74
+ const progressPercent = computed(() => {
75
+ return ((props.duration - remaining.value) / props.duration) * 100
76
+ })
77
+
78
+ const progressDashoffset = computed(() => {
79
+ const circumference = 2 * Math.PI * 45 // radius = 45
80
+ return circumference - (progressPercent.value / 100) * circumference
81
+ })
82
+
83
+ const isWarning = computed(() => {
84
+ return remaining.value <= 10 && remaining.value > 0
85
+ })
86
+
87
+ function start() {
88
+ if (isRunning.value && !isPaused.value) return
89
+
90
+ isRunning.value = true
91
+ isPaused.value = false
92
+
93
+ intervalId = setInterval(() => {
94
+ if (remaining.value > 0) {
95
+ remaining.value--
96
+ emit('tick', remaining.value)
97
+ }
98
+ if (remaining.value === 0) {
99
+ stop()
100
+ emit('complete')
101
+ }
102
+ }, 1000)
103
+ }
104
+
105
+ function pause() {
106
+ if (!isRunning.value || isPaused.value) return
107
+
108
+ isPaused.value = true
109
+ if (intervalId) {
110
+ clearInterval(intervalId)
111
+ intervalId = null
112
+ }
113
+ }
114
+
115
+ function resume() {
116
+ if (!isPaused.value) return
117
+ start()
118
+ }
119
+
120
+ function stop() {
121
+ isRunning.value = false
122
+ isPaused.value = false
123
+ if (intervalId) {
124
+ clearInterval(intervalId)
125
+ intervalId = null
126
+ }
127
+ }
128
+
129
+ function reset() {
130
+ stop()
131
+ remaining.value = props.duration
132
+ }
133
+
134
+ function toggle() {
135
+ if (!isRunning.value) {
136
+ start()
137
+ } else if (isPaused.value) {
138
+ resume()
139
+ } else {
140
+ pause()
141
+ }
142
+ }
143
+
144
+ let hasTriggered = false
145
+
146
+ // Autostart
147
+ if (props.autostart) {
148
+ start()
149
+ }
150
+
151
+ // Slidev keeps all slides in DOM. Detect slide activation via nav watcher.
152
+ onMounted(() => {
153
+ // Auto v-click registration: when at is undefined (default), register with click system
154
+ if (props.at === undefined) {
155
+ const ctx = clicksContext.value
156
+ if (ctx?.calculate && rootEl.value) {
157
+ const info = ctx.calculate('+1')
158
+ if (info) {
159
+ ctx.register(rootEl.value, info)
160
+ isAutoRegistered.value = true
161
+
162
+ const isActiveRef = info.isActive
163
+
164
+ watchEffect(() => {
165
+ const active = isActiveRef.value
166
+ if (active && !hasTriggered) {
167
+ hasTriggered = true
168
+ start()
169
+ } else if (!active && hasTriggered) {
170
+ hasTriggered = false
171
+ reset()
172
+ }
173
+ })
174
+ }
175
+ }
176
+ }
177
+
178
+ if (unref(slidePage) < 0) {
179
+ // Non-Slidev context fallback
180
+ if (props.at === undefined || props.at === 0) {
181
+ hasTriggered = true
182
+ start()
183
+ }
184
+ }
185
+ })
186
+
187
+ watch(() => slidevContext.nav?.currentSlideNo, (currentNo, prevNo) => {
188
+ const myPage = unref(slidePage)
189
+ if (myPage < 0) return
190
+
191
+ const isActive = currentNo + 1 === myPage
192
+ const wasActive = prevNo !== undefined && prevNo + 1 === myPage
193
+
194
+ if (isActive && !wasActive) {
195
+ hasTriggered = false
196
+ reset()
197
+ if (props.autostart || props.at === 0) {
198
+ hasTriggered = true
199
+ start()
200
+ }
201
+ } else if (!isActive && wasActive) {
202
+ stop()
203
+ }
204
+ }, { immediate: true })
205
+
206
+ // Watch clicks: passive fallback for explicit at > 0 (rare)
207
+ watch(() => clicksContext.value?.current, (current) => {
208
+ if (isAutoRegistered.value || props.at === undefined || props.at <= 0) return
209
+ const cur = current ?? 0
210
+
211
+ if (cur >= props.at && !hasTriggered) {
212
+ hasTriggered = true
213
+ start()
214
+ } else if (cur < props.at && hasTriggered) {
215
+ hasTriggered = false
216
+ reset()
217
+ }
218
+ })
219
+
220
+ // Watch for duration changes
221
+ watch(() => props.duration, (newDuration) => {
222
+ remaining.value = newDuration
223
+ })
224
+
225
+ onUnmounted(() => {
226
+ if (intervalId) {
227
+ clearInterval(intervalId)
228
+ }
229
+ if (isAutoRegistered.value && clicksContext.value?.unregister && rootEl.value) {
230
+ clicksContext.value.unregister(rootEl.value)
231
+ }
232
+ })
233
+
234
+ // Expose methods for external control
235
+ defineExpose({ start, pause, resume, stop, reset, toggle })
236
+ </script>
237
+
238
+ <template>
239
+ <div
240
+ ref="rootEl"
241
+ class="countdown"
242
+ :class="[
243
+ `countdown-size-${size}`,
244
+ `countdown-color-${color}`,
245
+ { 'countdown-warning': isWarning }
246
+ ]"
247
+ >
248
+ <!-- Circular progress ring -->
249
+ <div v-if="showProgress" class="countdown-ring">
250
+ <svg viewBox="0 0 100 100" class="countdown-ring-svg">
251
+ <!-- Background circle -->
252
+ <circle
253
+ cx="50"
254
+ cy="50"
255
+ r="45"
256
+ class="countdown-ring-bg"
257
+ />
258
+ <!-- Progress circle -->
259
+ <circle
260
+ cx="50"
261
+ cy="50"
262
+ r="45"
263
+ class="countdown-ring-progress"
264
+ :style="{ strokeDashoffset: progressDashoffset }"
265
+ />
266
+ </svg>
267
+ <div class="countdown-ring-content">
268
+ <span class="countdown-time">{{ formattedTime }}</span>
269
+ </div>
270
+ </div>
271
+
272
+ <!-- Simple display -->
273
+ <div v-else class="countdown-simple">
274
+ <span class="countdown-time">{{ formattedTime }}</span>
275
+ </div>
276
+
277
+ <!-- Controls -->
278
+ <div class="countdown-controls">
279
+ <button
280
+ class="countdown-btn"
281
+ @click="toggle"
282
+ :aria-label="isRunning && !isPaused ? 'Pause' : 'Start'"
283
+ >
284
+ <span v-if="!isRunning || isPaused">Start</span>
285
+ <span v-else>Pause</span>
286
+ </button>
287
+ <button
288
+ class="countdown-btn countdown-btn-secondary"
289
+ @click="reset"
290
+ aria-label="Reset"
291
+ >
292
+ Reset
293
+ </button>
294
+ </div>
295
+ </div>
296
+ </template>
297
+
298
+ <style>
299
+ .countdown {
300
+ display: flex;
301
+ flex-direction: column;
302
+ align-items: center;
303
+ gap: var(--space-md);
304
+ }
305
+
306
+ /* Size variants */
307
+ .countdown-size-sm .countdown-time {
308
+ font-size: var(--font-size-h2);
309
+ }
310
+
311
+ .countdown-size-md .countdown-time {
312
+ font-size: var(--font-size-h2);
313
+ }
314
+
315
+ .countdown-size-lg .countdown-time {
316
+ font-size: var(--font-size-h1);
317
+ }
318
+
319
+ .countdown-size-xl .countdown-time {
320
+ font-size: calc(var(--font-size-h1) * 1.5);
321
+ }
322
+
323
+ /* Time display */
324
+ .countdown-time {
325
+ font-family: var(--font-mono);
326
+ font-weight: var(--font-weight-bold);
327
+ font-variant-numeric: tabular-nums;
328
+ letter-spacing: 0.05em;
329
+ }
330
+
331
+ /* Color variants */
332
+ .countdown-color-primary .countdown-time {
333
+ color: var(--color-primary);
334
+ }
335
+
336
+ .countdown-color-danger .countdown-time {
337
+ color: var(--color-danger);
338
+ }
339
+
340
+ .countdown-color-warning .countdown-time {
341
+ color: var(--color-warning);
342
+ }
343
+
344
+ .countdown-color-success .countdown-time {
345
+ color: var(--color-success);
346
+ }
347
+
348
+ /* Warning animation (< 10 seconds) */
349
+ .countdown-warning .countdown-time {
350
+ animation: countdown-pulse 1s ease-in-out infinite;
351
+ }
352
+
353
+ @keyframes countdown-pulse {
354
+ 0%, 100% {
355
+ opacity: 1;
356
+ transform: scale(1);
357
+ }
358
+ 50% {
359
+ opacity: 0.7;
360
+ transform: scale(1.05);
361
+ }
362
+ }
363
+
364
+ /* Simple display */
365
+ .countdown-simple {
366
+ display: flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ }
370
+
371
+ /* Ring display */
372
+ .countdown-ring {
373
+ position: relative;
374
+ display: inline-flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ }
378
+
379
+ .countdown-size-sm .countdown-ring {
380
+ width: 120px;
381
+ height: 120px;
382
+ }
383
+
384
+ .countdown-size-md .countdown-ring {
385
+ width: 160px;
386
+ height: 160px;
387
+ }
388
+
389
+ .countdown-size-lg .countdown-ring {
390
+ width: 200px;
391
+ height: 200px;
392
+ }
393
+
394
+ .countdown-size-xl .countdown-ring {
395
+ width: 260px;
396
+ height: 260px;
397
+ }
398
+
399
+ .countdown-ring-svg {
400
+ width: 100%;
401
+ height: 100%;
402
+ transform: rotate(-90deg);
403
+ }
404
+
405
+ .countdown-ring-bg {
406
+ fill: none;
407
+ stroke: var(--color-border);
408
+ stroke-width: 6;
409
+ }
410
+
411
+ .countdown-ring-progress {
412
+ fill: none;
413
+ stroke: currentColor;
414
+ stroke-width: 6;
415
+ stroke-linecap: round;
416
+ stroke-dasharray: 282.74; /* 2 * PI * 45 */
417
+ transition: stroke-dashoffset 0.3s ease;
418
+ }
419
+
420
+ .countdown-color-primary .countdown-ring-progress {
421
+ stroke: var(--color-primary);
422
+ }
423
+
424
+ .countdown-color-danger .countdown-ring-progress {
425
+ stroke: var(--color-danger);
426
+ }
427
+
428
+ .countdown-color-warning .countdown-ring-progress {
429
+ stroke: var(--color-warning);
430
+ }
431
+
432
+ .countdown-color-success .countdown-ring-progress {
433
+ stroke: var(--color-success);
434
+ }
435
+
436
+ .countdown-ring-content {
437
+ position: absolute;
438
+ inset: 0;
439
+ display: flex;
440
+ align-items: center;
441
+ justify-content: center;
442
+ }
443
+
444
+ /* Ring text needs smaller font to fit inside circle */
445
+ .countdown-size-sm .countdown-ring .countdown-time { font-size: var(--font-size-base); }
446
+ .countdown-size-md .countdown-ring .countdown-time { font-size: 28px; }
447
+ .countdown-size-lg .countdown-ring .countdown-time { font-size: 40px; }
448
+ .countdown-size-xl .countdown-ring .countdown-time { font-size: var(--font-size-h1); }
449
+
450
+ /* Controls */
451
+ .countdown-controls {
452
+ display: flex;
453
+ gap: var(--space-sm);
454
+ }
455
+
456
+ .countdown-btn {
457
+ padding: var(--space-xs) var(--space-md);
458
+ font-size: var(--font-size-small);
459
+ font-weight: var(--font-weight-medium);
460
+ border: none;
461
+ border-radius: 0.375rem;
462
+ cursor: pointer;
463
+ background-color: var(--color-primary);
464
+ color: var(--color-primary-foreground);
465
+ transition: opacity 0.2s ease;
466
+ }
467
+
468
+ .countdown-btn:hover {
469
+ opacity: 0.9;
470
+ }
471
+
472
+ .countdown-btn-secondary {
473
+ background-color: var(--color-bg-muted);
474
+ color: var(--color-text);
475
+ }
476
+ </style>
@@ -0,0 +1,59 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Definition widget — term definition card.
4
+ *
5
+ * <Definition term="API" pronunciation="/ˈeɪ.piː.aɪ/" partOfSpeech="noun">
6
+ * A set of protocols for software communication.
7
+ * </Definition>
8
+ */
9
+
10
+ defineProps<{
11
+ term: string
12
+ pronunciation?: string
13
+ partOfSpeech?: string
14
+ }>()
15
+ </script>
16
+
17
+ <template>
18
+ <div class="definition">
19
+ <h1 class="definition-term">{{ term }}</h1>
20
+ <div v-if="pronunciation || partOfSpeech" class="definition-meta">
21
+ <span v-if="pronunciation" class="definition-pronunciation">{{ pronunciation }}</span>
22
+ <span v-if="partOfSpeech" class="definition-pos">{{ partOfSpeech }}</span>
23
+ </div>
24
+ <div class="definition-body">
25
+ <slot />
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <style scoped>
31
+ .definition-term {
32
+ color: var(--color-primary);
33
+ margin-bottom: var(--space-xs);
34
+ }
35
+
36
+ .definition-meta {
37
+ font-size: var(--font-size-small);
38
+ margin-bottom: var(--space-sm);
39
+ }
40
+
41
+ .definition-pronunciation {
42
+ font-family: var(--font-mono);
43
+ color: var(--color-text-secondary);
44
+ }
45
+
46
+ .definition-pos {
47
+ font-family: var(--font-sans);
48
+ color: var(--color-text-tertiary);
49
+ margin-left: 0.5em;
50
+ }
51
+
52
+ .definition-body :deep(h3) {
53
+ color: var(--color-primary);
54
+ font-size: var(--font-size-base);
55
+ border-top: 2px solid var(--color-border);
56
+ padding-top: var(--space-sm);
57
+ margin-top: var(--space-md);
58
+ }
59
+ </style>