@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,144 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * PricingTable widget — pricing comparison cards.
4
+ *
5
+ * <PricingTable :plans="[
6
+ * { name: 'Starter', price: '$9/mo', features: ['5 members', '10GB'] },
7
+ * { name: 'Pro', price: '$29/mo', features: ['25 members', '100GB'], highlighted: true },
8
+ * { name: 'Enterprise', price: '$99/mo', features: ['Unlimited', 'Dedicated'] }
9
+ * ]" />
10
+ */
11
+
12
+ type PricingFeature = string | { text: string; included?: boolean }
13
+
14
+ withDefaults(defineProps<{
15
+ plans: Array<{
16
+ name: string
17
+ price: string
18
+ features: PricingFeature[]
19
+ highlighted?: boolean
20
+ }>
21
+ checkColor?: 'primary' | 'success' | 'warning' | 'danger' | 'info'
22
+ }>(), {
23
+ checkColor: 'success',
24
+ })
25
+
26
+ function featureText(f: PricingFeature): string {
27
+ return typeof f === 'string' ? f : f.text
28
+ }
29
+
30
+ function featureIncluded(f: PricingFeature): boolean {
31
+ return typeof f === 'string' ? true : f.included !== false
32
+ }
33
+
34
+ const colorVar: Record<string, string> = {
35
+ primary: 'var(--color-primary)',
36
+ success: 'var(--color-success)',
37
+ warning: 'var(--color-warning)',
38
+ danger: 'var(--color-danger)',
39
+ info: 'var(--color-info)',
40
+ secondary: 'var(--color-secondary)',
41
+ accent: 'var(--color-accent)',
42
+ }
43
+ </script>
44
+
45
+ <template>
46
+ <div class="pricing-table">
47
+ <div
48
+ v-for="(plan, i) in plans"
49
+ :key="i"
50
+ class="pricing-plan"
51
+ :class="{ 'pricing-plan-highlighted': plan.highlighted }"
52
+ >
53
+ <div class="pricing-plan-header">
54
+ <div class="pricing-plan-name">{{ plan.name }}</div>
55
+ <div class="pricing-plan-price">{{ plan.price }}</div>
56
+ </div>
57
+ <ul class="pricing-plan-features">
58
+ <li
59
+ v-for="(feature, j) in plan.features"
60
+ :key="j"
61
+ :class="{ 'feature-excluded': !featureIncluded(feature) }"
62
+ :style="{ '--check-color': colorVar[checkColor] }"
63
+ >
64
+ {{ featureText(feature) }}
65
+ </li>
66
+ </ul>
67
+ </div>
68
+ </div>
69
+ </template>
70
+
71
+ <style scoped>
72
+ .pricing-table {
73
+ display: flex;
74
+ gap: var(--space-sm);
75
+ margin-top: var(--space-lg);
76
+ }
77
+
78
+ .pricing-plan {
79
+ flex: 1;
80
+ background: var(--color-bg-soft);
81
+ border: 1px solid var(--color-border);
82
+ border-radius: 0.5rem;
83
+ padding: var(--space-md);
84
+ }
85
+
86
+ .pricing-plan-highlighted {
87
+ border-color: var(--color-primary);
88
+ border-width: 2px;
89
+ transform: scale(1.03);
90
+ box-shadow: 0 4px 16px color-mix(in srgb, var(--color-black) 10%, transparent);
91
+ }
92
+
93
+ .dark .pricing-plan-highlighted {
94
+ box-shadow: 0 4px 16px color-mix(in srgb, var(--color-black) 30%, transparent);
95
+ }
96
+
97
+ .pricing-plan-header {
98
+ border-bottom: 2px solid var(--color-border);
99
+ padding-bottom: var(--space-xs);
100
+ margin-bottom: var(--space-sm);
101
+ }
102
+
103
+ .pricing-plan-name {
104
+ font-size: var(--font-size-base);
105
+ font-weight: var(--font-weight-semibold);
106
+ }
107
+
108
+ .pricing-plan-price {
109
+ font-size: var(--font-size-h2);
110
+ font-weight: var(--font-weight-bold);
111
+ color: var(--color-primary);
112
+ margin-top: var(--space-xs);
113
+ }
114
+
115
+ .pricing-plan-features {
116
+ list-style: none !important;
117
+ margin: 0;
118
+ padding: 0;
119
+ }
120
+
121
+ .pricing-plan-features li {
122
+ list-style: none !important;
123
+ padding-left: 1.5em;
124
+ position: relative;
125
+ margin-block: 0.5em;
126
+ }
127
+
128
+ .pricing-plan-features li::before {
129
+ content: '\2713';
130
+ position: absolute;
131
+ left: 0;
132
+ color: var(--check-color);
133
+ font-weight: var(--font-weight-bold);
134
+ }
135
+
136
+ .pricing-plan-features li.feature-excluded {
137
+ color: var(--color-text-tertiary);
138
+ }
139
+
140
+ .pricing-plan-features li.feature-excluded::before {
141
+ content: '\2717';
142
+ color: var(--color-text-tertiary);
143
+ }
144
+ </style>
@@ -0,0 +1,100 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Progress component - presentation progress bar
4
+ *
5
+ * Usage:
6
+ * <Progress />
7
+ * <Progress position="bottom" thickness="4px" />
8
+ * <Progress showPercentage />
9
+ */
10
+
11
+ import { computed } from 'vue'
12
+ import { useNav } from '@slidev/client'
13
+
14
+ const props = defineProps<{
15
+ position?: 'top' | 'bottom'
16
+ color?: string
17
+ thickness?: string
18
+ opacity?: number
19
+ showPercentage?: boolean
20
+ }>()
21
+
22
+ const { currentSlideNo, total } = useNav()
23
+
24
+ const progress = computed(() => {
25
+ return Math.round((currentSlideNo.value / total.value) * 100)
26
+ })
27
+
28
+ const positionClass = computed(() => {
29
+ return props.position === 'bottom' ? 'progress-bottom' : 'progress-top'
30
+ })
31
+
32
+ const barStyle = computed(() => {
33
+ const styles: Record<string, string> = {
34
+ width: `${progress.value}%`
35
+ }
36
+ if (props.color) {
37
+ styles.backgroundColor = props.color
38
+ }
39
+ if (props.thickness) {
40
+ styles.height = props.thickness
41
+ }
42
+ if (props.opacity !== undefined) {
43
+ styles.opacity = String(props.opacity)
44
+ }
45
+ return styles
46
+ })
47
+ </script>
48
+
49
+ <template>
50
+ <div class="progress-container" :class="positionClass">
51
+ <div class="progress-bar" :style="barStyle"></div>
52
+ <span v-if="showPercentage" class="progress-percentage">{{ progress }}%</span>
53
+ </div>
54
+ </template>
55
+
56
+ <style>
57
+ .progress-container {
58
+ position: fixed;
59
+ left: 0;
60
+ right: 0;
61
+ z-index: 100;
62
+ display: flex;
63
+ align-items: center;
64
+ pointer-events: none;
65
+ }
66
+
67
+ .progress-top {
68
+ top: 0;
69
+ }
70
+
71
+ .progress-bottom {
72
+ bottom: 0;
73
+ }
74
+
75
+ .progress-bar {
76
+ height: 3px;
77
+ background-color: var(--color-primary);
78
+ opacity: 0.8;
79
+ transition: width var(--dur-moderate-01) var(--ease-standard);
80
+ }
81
+
82
+ .progress-percentage {
83
+ position: absolute;
84
+ right: 1rem;
85
+ font-size: var(--font-size-small);
86
+ font-weight: var(--font-weight-medium);
87
+ color: var(--color-text-secondary);
88
+ background-color: var(--color-bg);
89
+ padding: 0.125rem 0.5rem;
90
+ border-radius: 0.25rem;
91
+ }
92
+
93
+ .progress-top .progress-percentage {
94
+ top: 0.5rem;
95
+ }
96
+
97
+ .progress-bottom .progress-percentage {
98
+ bottom: 0.5rem;
99
+ }
100
+ </style>
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Pyramid widget — hierarchical level visualization.
4
+ *
5
+ * <Pyramid :levels="[
6
+ * { label: 'Strategy', description: 'Vision' },
7
+ * { label: 'Tactics', description: 'Plans' },
8
+ * { label: 'Operations', description: 'Execution' }
9
+ * ]" />
10
+ */
11
+
12
+ const props = withDefaults(defineProps<{
13
+ levels: Array<{ label: string; description?: string }>
14
+ colors?: string[]
15
+ }>(), {
16
+ colors: () => ['primary', 'info', 'success', 'warning'],
17
+ })
18
+
19
+ const colorVarMap: Record<string, string> = {
20
+ primary: 'var(--color-primary)',
21
+ success: 'var(--color-success)',
22
+ warning: 'var(--color-warning)',
23
+ danger: 'var(--color-danger)',
24
+ info: 'var(--color-info)',
25
+ secondary: 'var(--color-secondary)',
26
+ accent: 'var(--color-accent)',
27
+ }
28
+
29
+ function levelColor(index: number): string {
30
+ const c = props.colors[index % props.colors.length]
31
+ return colorVarMap[c] || colorVarMap.primary
32
+ }
33
+
34
+ function levelFontSize(index: number, total: number): string {
35
+ const base = 1.5 // rem base
36
+ const scale = 1 + (index / Math.max(total - 1, 1)) * 0.3
37
+ return `${base * scale}rem`
38
+ }
39
+ </script>
40
+
41
+ <template>
42
+ <div class="pyramid">
43
+ <div
44
+ v-for="(level, i) in levels"
45
+ :key="i"
46
+ class="pyramid-level"
47
+ :style="{ borderTopColor: levelColor(i) }"
48
+ >
49
+ <div class="pyramid-level-label">{{ level.label }}</div>
50
+ <div
51
+ v-if="level.description"
52
+ class="pyramid-level-desc"
53
+ :style="{ fontSize: levelFontSize(i, levels.length) }"
54
+ >{{ level.description }}</div>
55
+ </div>
56
+ </div>
57
+ </template>
58
+
59
+ <style scoped>
60
+ .pyramid {
61
+ display: flex;
62
+ gap: var(--space-sm);
63
+ margin-top: var(--space-lg);
64
+ }
65
+
66
+ .pyramid-level {
67
+ flex: 1;
68
+ text-align: center;
69
+ border-top: 3px solid;
70
+ padding-top: var(--space-xs);
71
+ }
72
+
73
+ .pyramid-level-label {
74
+ font-weight: var(--font-weight-semibold);
75
+ margin-bottom: var(--space-xs);
76
+ }
77
+
78
+ .pyramid-level-desc {
79
+ color: var(--color-text-secondary);
80
+ }
81
+ </style>
@@ -0,0 +1,137 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * QRCode component - generates QR codes as SVG
4
+ *
5
+ * Theme-aware by default: uses text color from current theme.
6
+ * Automatically updates when theme changes (T key).
7
+ *
8
+ * Usage:
9
+ * <QRCode data="https://example.com" />
10
+ * <QRCode data="https://example.com" :size="200" />
11
+ * <QRCode data="https://example.com" color="accent" />
12
+ *
13
+ * Requires: npm install qrcode (peer dependency)
14
+ */
15
+
16
+ import { ref, watch, onMounted, computed } from 'vue'
17
+ import { useIsDark } from '../composables/useColors'
18
+ import QRCodeLib from 'qrcode'
19
+
20
+ const props = withDefaults(defineProps<{
21
+ data: string
22
+ size?: number
23
+ color?: string // Semantic color name (primary, success, accent, etc.) or 'text' (default)
24
+ bgColor?: string // Semantic bg name (bg, bg-soft, bg-muted) or omit for 'bg' (default)
25
+ }>(), {
26
+ size: 200
27
+ })
28
+
29
+ const isDark = useIsDark()
30
+ const svgString = ref('')
31
+ const error = ref<string | null>(null)
32
+
33
+ // Read resolved color value from CSS variable at runtime
34
+ function getCssColor(varName: string): string {
35
+ const value = getComputedStyle(document.documentElement).getPropertyValue(varName).trim()
36
+ if (!value) throw new Error(`CSS variable ${varName} is empty - check styles/colors.css`)
37
+ return value
38
+ }
39
+
40
+ // Map semantic color name to CSS variable name
41
+ function colorToVar(name: string | undefined, fallbackVar: string): string {
42
+ if (!name) return fallbackVar
43
+ // Already a var name
44
+ if (name.startsWith('--')) return name
45
+ // Map semantic names to CSS vars
46
+ return `--color-${name}`
47
+ }
48
+
49
+ // Resolve QR code color based on theme and props
50
+ const resolvedColor = computed(() => {
51
+ return getCssColor(colorToVar(props.color, '--color-text'))
52
+ })
53
+
54
+ // Resolve background color based on theme and props
55
+ const resolvedBgColor = computed(() => {
56
+ return getCssColor(colorToVar(props.bgColor, '--color-bg'))
57
+ })
58
+
59
+ async function generateQR() {
60
+ try {
61
+ error.value = null
62
+
63
+ svgString.value = await QRCodeLib.toString(props.data, {
64
+ type: 'svg',
65
+ width: props.size,
66
+ margin: 0,
67
+ color: {
68
+ dark: resolvedColor.value,
69
+ light: resolvedBgColor.value
70
+ },
71
+ errorCorrectionLevel: 'M'
72
+ })
73
+ } catch (e) {
74
+ error.value = e instanceof Error ? e.message : 'Failed to generate QR code'
75
+ console.error('QRCode generation error:', e)
76
+ }
77
+ }
78
+
79
+ onMounted(generateQR)
80
+
81
+ // Regenerate QR code when any relevant prop or theme changes
82
+ watch(
83
+ () => [
84
+ props.data,
85
+ props.size,
86
+ props.color,
87
+ props.bgColor,
88
+ isDark.value
89
+ ],
90
+ generateQR
91
+ )
92
+ </script>
93
+
94
+ <template>
95
+ <div
96
+ class="qrcode"
97
+ :class="{ 'qrcode-error': error }"
98
+ :style="{ width: `${size}px`, height: `${size}px` }"
99
+ >
100
+ <div v-if="error" class="qrcode-error-msg">{{ error }}</div>
101
+ <div v-else v-html="svgString" class="qrcode-svg"></div>
102
+ </div>
103
+ </template>
104
+
105
+ <style>
106
+ .qrcode {
107
+ display: inline-block;
108
+ line-height: 0;
109
+ }
110
+
111
+ .qrcode-svg {
112
+ width: 100%;
113
+ height: 100%;
114
+ }
115
+
116
+ .qrcode-svg svg {
117
+ display: block;
118
+ width: 100%;
119
+ height: 100%;
120
+ }
121
+
122
+ .qrcode-error {
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ background-color: var(--color-bg-muted);
127
+ border-radius: 0.25rem;
128
+ }
129
+
130
+ .qrcode-error-msg {
131
+ font-size: var(--font-size-small);
132
+ color: var(--color-danger);
133
+ text-align: center;
134
+ padding: 0.5rem;
135
+ line-height: var(--line-height-body);
136
+ }
137
+ </style>
@@ -0,0 +1,101 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * QuoteBlock primitive — large centered quote with decorative quotation mark.
4
+ *
5
+ * <QuoteBlock author="Steve Jobs" source="Stanford, 2005">
6
+ * Stay hungry, stay foolish.
7
+ * </QuoteBlock>
8
+ */
9
+
10
+ withDefaults(defineProps<{
11
+ author?: string
12
+ source?: string
13
+ size?: 'md' | 'lg'
14
+ mark?: boolean
15
+ }>(), {
16
+ size: 'lg',
17
+ mark: true,
18
+ })
19
+ </script>
20
+
21
+ <template>
22
+ <div class="quote-block" :class="`quote-block-${size}`">
23
+ <div class="quote-block-text">
24
+ <slot />
25
+ </div>
26
+ <div v-if="author || source" class="quote-block-attribution">
27
+ <span v-if="author" class="quote-block-author">{{ author }}</span>
28
+ <span v-if="source" class="quote-block-source">{{ source }}</span>
29
+ </div>
30
+ </div>
31
+ </template>
32
+
33
+ <style scoped>
34
+ .quote-block {
35
+ text-align: center;
36
+ max-width: 35ch;
37
+ margin-inline: auto;
38
+ position: relative;
39
+ }
40
+
41
+ .quote-block-text {
42
+ font-weight: var(--font-weight-medium);
43
+ line-height: var(--line-height-body);
44
+ color: var(--color-text);
45
+ position: relative;
46
+ }
47
+
48
+ .quote-block-text::before {
49
+ content: '\201C';
50
+ font-size: 6rem;
51
+ line-height: 1;
52
+ color: var(--color-primary);
53
+ opacity: 0.3;
54
+ position: absolute;
55
+ top: -2.5rem;
56
+ left: -1rem;
57
+ }
58
+
59
+ .quote-block-lg .quote-block-text {
60
+ font-size: calc(var(--font-size-h2) * 0.85);
61
+ }
62
+
63
+ .quote-block-md .quote-block-text {
64
+ font-size: calc(var(--font-size-base) * 1.15);
65
+ }
66
+
67
+ .quote-block-text :deep(p) {
68
+ text-align: center;
69
+ max-width: none;
70
+ }
71
+
72
+ .quote-block-text :deep(blockquote) {
73
+ border: none;
74
+ padding: 0;
75
+ margin: 0;
76
+ font-size: inherit;
77
+ font-weight: inherit;
78
+ color: inherit;
79
+ }
80
+
81
+ .quote-block-text :deep(blockquote p) {
82
+ text-align: center;
83
+ max-width: none;
84
+ }
85
+
86
+ .quote-block-attribution {
87
+ font-size: var(--font-size-small);
88
+ color: var(--color-text-secondary);
89
+ margin-top: var(--space-md);
90
+ }
91
+
92
+ .quote-block-author {
93
+ display: block;
94
+ }
95
+
96
+ .quote-block-source {
97
+ display: block;
98
+ color: var(--color-text-tertiary);
99
+ font-size: calc(var(--font-size-small) * 0.9);
100
+ }
101
+ </style>