@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,204 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Background primitive — semantic wrapper for bg slots.
4
+ * Replaces <div class="bg-primary"></div> and raw ![](image) in bg slots.
5
+ *
6
+ * Gradient prop:
7
+ * 1. Presets (string): "accent" (colorful brand), "subtle" (neutral)
8
+ * 2. Full control (array): [type, direction, ...colorStops]
9
+ * - type: 'linear' | 'radial' | 'conic'
10
+ * - direction: CSS direction ('135deg', 'to right', 'circle', 'from 45deg')
11
+ * - colorStops: [semanticColor, position] tuples
12
+ *
13
+ * Examples:
14
+ * gradient="accent"
15
+ * :gradient="['linear', '135deg', ['primary', '0%'], ['info', '100%']]"
16
+ * :gradient="['radial', 'circle', ['primary', '0%'], ['soft', '100%']]"
17
+ * :gradient="['linear', 'to right', ['danger', '0%'], ['warning', '50%'], ['success', '100%']]"
18
+ */
19
+
20
+ import { computed, onMounted, ref } from 'vue'
21
+ import type { BgColor } from '../composables/useColors'
22
+
23
+ type ColorStop = [string, string]
24
+ type GradientArray = [string, string, ...ColorStop[]]
25
+
26
+ const props = defineProps<{
27
+ src?: string
28
+ fit?: 'cover' | 'contain'
29
+ color?: BgColor
30
+ gradient?: string | GradientArray
31
+ bgOverlay?: 'dark' | 'light' | 'none'
32
+ opacity?: number
33
+ }>()
34
+
35
+ const colorVarMap: Record<string, string> = {
36
+ primary: 'var(--color-primary)',
37
+ success: 'var(--color-success)',
38
+ warning: 'var(--color-warning)',
39
+ danger: 'var(--color-danger)',
40
+ info: 'var(--color-info)',
41
+ secondary: 'var(--color-secondary)',
42
+ accent: 'var(--color-accent)',
43
+ soft: 'var(--color-bg-soft)',
44
+ muted: 'var(--color-bg-muted)',
45
+ }
46
+
47
+ function resolveColor(name: string): string {
48
+ return colorVarMap[name] || name
49
+ }
50
+
51
+ const gradientStyle = computed(() => {
52
+ const g = props.gradient
53
+ if (!g) return undefined
54
+
55
+ // String presets
56
+ if (typeof g === 'string') {
57
+ if (g === 'accent') return { background: 'var(--gradient-accent)' }
58
+ if (g === 'subtle') return { background: 'var(--gradient-section)' }
59
+ return undefined
60
+ }
61
+
62
+ // Array: [type, direction, ...colorStops]
63
+ if (Array.isArray(g) && g.length >= 3) {
64
+ const [type, direction, ...stops] = g
65
+ const cssStops = stops
66
+ .map((s: ColorStop) => `${resolveColor(s[0])} ${s[1]}`)
67
+ .join(', ')
68
+ return { background: `${type}-gradient(${direction}, ${cssStops})` }
69
+ }
70
+
71
+ return undefined
72
+ })
73
+
74
+ const isVideo = props.src?.match(/\.(mp4|webm|ogg)(\?|$)/i)
75
+
76
+ // --- Auto luminance detection for gradients ---
77
+ // When gradient is used without bgOverlay, detect luminance at mount
78
+ // and add a hidden marker class so :has() selectors in base.css
79
+ // auto-adjust text color.
80
+
81
+ const bgEl = ref<HTMLElement | null>(null)
82
+ const luminanceHint = ref<string | null>(null)
83
+
84
+ function parseRgb(raw: string): [number, number, number] | null {
85
+ raw = raw.trim()
86
+ if (raw.startsWith('#')) {
87
+ const hex = raw.length === 4
88
+ ? raw[1] + raw[1] + raw[2] + raw[2] + raw[3] + raw[3]
89
+ : raw.slice(1, 7)
90
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]
91
+ }
92
+ const m = raw.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
93
+ if (m) return [+m[1], +m[2], +m[3]]
94
+ return null
95
+ }
96
+
97
+ function relativeLuminance(r: number, g: number, b: number): number {
98
+ const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(c =>
99
+ c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4),
100
+ )
101
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
102
+ }
103
+
104
+ function detectLuminance() {
105
+ // Only for color/gradient backgrounds without explicit bgOverlay or image
106
+ if (props.bgOverlay || props.src) return
107
+ if (!props.gradient) return
108
+
109
+ const el = bgEl.value
110
+ if (!el) return
111
+
112
+ // Read the resolved background from the rendered element
113
+ const style = getComputedStyle(el)
114
+ const bg = style.background || style.backgroundImage || style.backgroundColor || ''
115
+
116
+ // Parse all rgb() values from the resolved background
117
+ const rgbRegex = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/g
118
+ const luminances: number[] = []
119
+ let match
120
+ while ((match = rgbRegex.exec(bg))) {
121
+ luminances.push(relativeLuminance(+match[1], +match[2], +match[3]))
122
+ }
123
+
124
+ if (luminances.length === 0) return
125
+
126
+ // Average luminance across all gradient stops
127
+ const avg = luminances.reduce((a, b) => a + b, 0) / luminances.length
128
+ // 0.18 = crossover point where neither black nor white gives 4.5:1
129
+ // Above 0.18: black text is better (bg-overlay-light)
130
+ // Below 0.18: white text is better (bg-overlay-dark)
131
+ luminanceHint.value = avg > 0.18 ? 'bg-overlay-light' : 'bg-overlay-dark'
132
+ }
133
+
134
+ onMounted(detectLuminance)
135
+ </script>
136
+
137
+ <template>
138
+ <div
139
+ ref="bgEl"
140
+ class="background-fill"
141
+ :class="[
142
+ color ? `bg-${color}` : '',
143
+ ]"
144
+ :style="{
145
+ ...gradientStyle,
146
+ ...(opacity !== undefined && opacity !== 1 ? { opacity } : {}),
147
+ }"
148
+ >
149
+ <video
150
+ v-if="src && isVideo"
151
+ :src="src"
152
+ autoplay
153
+ loop
154
+ muted
155
+ playsinline
156
+ :style="{ objectFit: fit || 'cover' }"
157
+ />
158
+ <img
159
+ v-else-if="src"
160
+ :src="src"
161
+ alt=""
162
+ :style="{ objectFit: fit || 'cover' }"
163
+ />
164
+ <div
165
+ v-if="bgOverlay && bgOverlay !== 'none' && src"
166
+ class="bg-overlay"
167
+ :class="`bg-overlay-${bgOverlay}`"
168
+ />
169
+ <!-- Luminance hint: hidden marker for :has() text color detection -->
170
+ <span v-if="luminanceHint" :class="luminanceHint" style="display: none" aria-hidden="true" />
171
+ </div>
172
+ </template>
173
+
174
+ <style scoped>
175
+ .background-fill {
176
+ width: 100%;
177
+ height: 100%;
178
+ position: relative;
179
+ }
180
+
181
+ .background-fill img,
182
+ .background-fill video {
183
+ width: 100%;
184
+ height: 100%;
185
+ display: block;
186
+ }
187
+ </style>
188
+
189
+ <!-- Overlay classes must be global so :has() in base.css can detect them -->
190
+ <style>
191
+ .bg-overlay {
192
+ position: absolute;
193
+ inset: 0;
194
+ z-index: 1;
195
+ }
196
+
197
+ .bg-overlay-dark {
198
+ background: linear-gradient(to bottom, color-mix(in srgb, var(--color-black) 40%, transparent) 0%, color-mix(in srgb, var(--color-black) 60%, transparent) 100%);
199
+ }
200
+
201
+ .bg-overlay-light {
202
+ background: linear-gradient(to bottom, color-mix(in srgb, var(--color-white) 70%, transparent) 0%, color-mix(in srgb, var(--color-white) 85%, transparent) 100%);
203
+ }
204
+ </style>
@@ -0,0 +1,135 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Callout component - admonition/alert boxes
4
+ *
5
+ * Usage:
6
+ * <Callout type="info" title="Note">Content here</Callout>
7
+ * <Callout type="warning">Warning text</Callout>
8
+ * <Callout type="tip">Pro tip!</Callout>
9
+ * <Callout type="danger">Be careful</Callout>
10
+ */
11
+
12
+ defineProps<{
13
+ type?: 'info' | 'tip' | 'warning' | 'danger' | 'note'
14
+ title?: string
15
+ }>()
16
+ </script>
17
+
18
+ <template>
19
+ <div class="callout" :class="`callout-${type || 'info'}`">
20
+ <div class="callout-header" v-if="title">
21
+ <span class="callout-icon">
22
+ <svg v-if="type === 'tip'" viewBox="0 0 24 24" fill="currentColor">
23
+ <path d="M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17a1 1 0 001 1h6a1 1 0 001-1v-2.26c1.81-1.27 3-3.36 3-5.74 0-3.87-3.13-7-7-7zm2 14H10v-1h4v1zm0-2H10v-1h4v1zm1.03-3.27l-.7.49V12h-4.56v-.78l-.7-.49C8.27 9.65 7.5 8.38 7.5 9c0-2.48 2.02-4.5 4.5-4.5s4.5 2.02 4.5 4.5c0 1.38-.77 2.65-1.97 3.73z"/>
24
+ </svg>
25
+ <svg v-else-if="type === 'warning'" viewBox="0 0 24 24" fill="currentColor">
26
+ <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
27
+ </svg>
28
+ <svg v-else-if="type === 'danger'" viewBox="0 0 24 24" fill="currentColor">
29
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
30
+ </svg>
31
+ <svg v-else viewBox="0 0 24 24" fill="currentColor">
32
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
33
+ </svg>
34
+ </span>
35
+ <span class="callout-title">{{ title }}</span>
36
+ </div>
37
+ <div class="callout-content">
38
+ <slot />
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <style>
44
+ .callout {
45
+ padding: 1rem 1rem 1rem 1.25rem;
46
+ margin-block: 1rem;
47
+ background-color: var(--color-bg-soft);
48
+ border-left: 4px solid var(--color-info);
49
+ border-radius: 0 0.375rem 0.375rem 0;
50
+ }
51
+
52
+ .callout-header {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 0.5rem;
56
+ margin-bottom: 0.5rem;
57
+ font-weight: var(--font-weight-semibold);
58
+ }
59
+
60
+ .callout-icon {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ width: 1.25rem;
65
+ height: 1.25rem;
66
+ }
67
+
68
+ .callout-icon svg {
69
+ width: 100%;
70
+ height: 100%;
71
+ }
72
+
73
+ .callout-title {
74
+ font-size: var(--font-size-base);
75
+ }
76
+
77
+ .callout-content {
78
+ font-size: var(--font-size-base);
79
+ line-height: var(--line-height-body);
80
+ }
81
+
82
+ .callout-content p:first-child {
83
+ margin-top: 0;
84
+ }
85
+
86
+ .callout-content p:last-child {
87
+ margin-bottom: 0;
88
+ }
89
+
90
+ /* Type variants */
91
+ .callout-info {
92
+ border-left-color: var(--color-info);
93
+ }
94
+
95
+ .callout-info .callout-icon,
96
+ .callout-info .callout-title {
97
+ color: var(--color-info);
98
+ }
99
+
100
+ .callout-tip {
101
+ border-left-color: var(--color-success);
102
+ }
103
+
104
+ .callout-tip .callout-icon,
105
+ .callout-tip .callout-title {
106
+ color: var(--color-success);
107
+ }
108
+
109
+ .callout-warning {
110
+ border-left-color: var(--color-warning);
111
+ }
112
+
113
+ .callout-warning .callout-icon,
114
+ .callout-warning .callout-title {
115
+ color: var(--color-warning);
116
+ }
117
+
118
+ .callout-danger {
119
+ border-left-color: var(--color-danger);
120
+ }
121
+
122
+ .callout-danger .callout-icon,
123
+ .callout-danger .callout-title {
124
+ color: var(--color-danger);
125
+ }
126
+
127
+ .callout-note {
128
+ border-left-color: var(--color-primary);
129
+ }
130
+
131
+ .callout-note .callout-icon,
132
+ .callout-note .callout-title {
133
+ color: var(--color-primary);
134
+ }
135
+ </style>
@@ -0,0 +1,75 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Card component - individual card for CardGrid
4
+ *
5
+ * Usage:
6
+ * <Card title="Fast" icon="ph-lightning-fill">Description text</Card>
7
+ * <Card title="Secure">Description without icon</Card>
8
+ */
9
+
10
+ import { computed } from 'vue'
11
+
12
+ const props = defineProps<{
13
+ title: string
14
+ icon?: string
15
+ }>()
16
+
17
+ const isPhosphorIcon = computed(() => props.icon?.startsWith('ph-'))
18
+ </script>
19
+
20
+ <template>
21
+ <div class="card">
22
+ <div class="card-icon" v-if="icon">
23
+ <Icon v-if="isPhosphorIcon" :name="icon" size="lg" />
24
+ <span v-else>{{ icon }}</span>
25
+ </div>
26
+ <h4 class="card-title">{{ title }}</h4>
27
+ <div class="card-content">
28
+ <slot />
29
+ </div>
30
+ </div>
31
+ </template>
32
+
33
+ <style>
34
+ .card {
35
+ padding: 1.25rem;
36
+ background-color: var(--color-bg-soft);
37
+ border-radius: 0.5rem;
38
+ border: 1px solid var(--color-border);
39
+ transition:
40
+ border-color var(--dur-fast-02) var(--ease-standard),
41
+ box-shadow var(--dur-fast-02) var(--ease-standard);
42
+ }
43
+
44
+ .card:hover {
45
+ border-color: var(--color-border-strong);
46
+ box-shadow: 0 2px 8px color-mix(in srgb, var(--color-black) 5%, transparent);
47
+ }
48
+
49
+ .dark .card:hover {
50
+ box-shadow: 0 2px 8px color-mix(in srgb, var(--color-black) 20%, transparent);
51
+ }
52
+
53
+ .card-icon {
54
+ font-size: 2rem;
55
+ margin-bottom: 0.75rem;
56
+ line-height: 1;
57
+ }
58
+
59
+ .card-title {
60
+ font-size: var(--font-size-base);
61
+ font-weight: var(--font-weight-semibold);
62
+ margin: 0 0 0.5rem 0;
63
+ color: var(--color-text);
64
+ }
65
+
66
+ .card-content {
67
+ font-size: var(--font-size-small);
68
+ color: var(--color-text-secondary);
69
+ line-height: var(--line-height-body);
70
+ }
71
+
72
+ .card-content p {
73
+ margin: 0;
74
+ }
75
+ </style>
@@ -0,0 +1,67 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CardGrid component - responsive grid layout for Card components
4
+ *
5
+ * Usage:
6
+ * <CardGrid cols="2">
7
+ * <Card title="Fast" icon="🚀">Description</Card>
8
+ * <Card title="Secure" icon="🔒">Description</Card>
9
+ * </CardGrid>
10
+ *
11
+ * <CardGrid cols="3">
12
+ * <Card title="A">...</Card>
13
+ * <Card title="B">...</Card>
14
+ * <Card title="C">...</Card>
15
+ * </CardGrid>
16
+ */
17
+
18
+ defineProps<{
19
+ cols?: '2' | '3' | '4'
20
+ gap?: 'sm' | 'md' | 'lg'
21
+ }>()
22
+ </script>
23
+
24
+ <template>
25
+ <div
26
+ class="card-grid"
27
+ :class="[
28
+ `card-grid-cols-${cols || '2'}`,
29
+ `card-grid-gap-${gap || 'md'}`
30
+ ]"
31
+ >
32
+ <slot />
33
+ </div>
34
+ </template>
35
+
36
+ <style>
37
+ .card-grid {
38
+ display: grid;
39
+ margin-block: 1rem;
40
+ }
41
+
42
+ /* Column variants */
43
+ .card-grid-cols-2 {
44
+ grid-template-columns: repeat(2, 1fr);
45
+ }
46
+
47
+ .card-grid-cols-3 {
48
+ grid-template-columns: repeat(3, 1fr);
49
+ }
50
+
51
+ .card-grid-cols-4 {
52
+ grid-template-columns: repeat(4, 1fr);
53
+ }
54
+
55
+ /* Gap variants */
56
+ .card-grid-gap-sm {
57
+ gap: var(--space-sm);
58
+ }
59
+
60
+ .card-grid-gap-md {
61
+ gap: var(--space-md);
62
+ }
63
+
64
+ .card-grid-gap-lg {
65
+ gap: var(--space-lg);
66
+ }
67
+ </style>
@@ -0,0 +1,66 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CaseStudy widget — sections with colored left borders.
4
+ *
5
+ * <CaseStudy :sections="[
6
+ * { title: 'Challenge', content: '...', color: 'danger' },
7
+ * { title: 'Solution', content: '...', color: 'primary' },
8
+ * { title: 'Result', content: '...', color: 'success' }
9
+ * ]" />
10
+ */
11
+
12
+ import type { SemanticColor } from '../composables/useColors'
13
+ import { semanticColorVar } from '../composables/useColors'
14
+
15
+ defineProps<{
16
+ sections: Array<{
17
+ title: string
18
+ content: string
19
+ color?: SemanticColor
20
+ }>
21
+ }>()
22
+ </script>
23
+
24
+ <template>
25
+ <div class="case-study">
26
+ <div
27
+ v-for="(section, i) in sections"
28
+ :key="i"
29
+ class="case-study-section"
30
+ :style="{ '--section-color': semanticColorVar[section.color || 'primary'] }"
31
+ >
32
+ <h3 class="case-study-title">{{ section.title }}</h3>
33
+ <p class="case-study-content">{{ section.content }}</p>
34
+ </div>
35
+ </div>
36
+ </template>
37
+
38
+ <style scoped>
39
+ .case-study {
40
+ display: flex;
41
+ gap: var(--space-sm);
42
+ margin-top: var(--space-lg);
43
+ }
44
+
45
+ .case-study-section {
46
+ flex: 1;
47
+ background: var(--color-bg-soft);
48
+ border-radius: 0.5rem;
49
+ padding: var(--space-sm);
50
+ border-left: 4px solid var(--section-color);
51
+ }
52
+
53
+ .case-study-title {
54
+ margin: 0 0 var(--space-xs);
55
+ font-size: var(--font-size-base);
56
+ font-weight: var(--font-weight-semibold);
57
+ padding-left: var(--space-xs);
58
+ }
59
+
60
+ .case-study-content {
61
+ font-size: var(--font-size-small);
62
+ color: var(--color-text-secondary);
63
+ margin: 0;
64
+ max-width: none;
65
+ }
66
+ </style>