@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,436 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ImageCompare component - before/after image slider comparison
4
+ *
5
+ * Usage:
6
+ * <ImageCompare before="/before.png" after="/after.png" />
7
+ * <ImageCompare before="/old.jpg" after="/new.jpg" :target="30" />
8
+ * <ImageCompare before="/a.png" after="/b.png" direction="vertical" />
9
+ * <ImageCompare before="/x.jpg" after="/y.jpg" :labels="{ before: 'Old', after: 'New' }" />
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
+ const props = withDefaults(defineProps<{
19
+ before: string
20
+ after: string
21
+ target?: number // 0-100, target slider position (animation end point)
22
+ direction?: 'horizontal' | 'vertical'
23
+ labels?: { before?: string; after?: string }
24
+ height?: string // CSS height (e.g., '20rem', '300px')
25
+ at?: number // undefined = auto-register click, 0 = on slide enter, -1 = manual
26
+ animDuration?: number // animation duration in ms
27
+ }>(), {
28
+ target: 50,
29
+ direction: 'horizontal',
30
+ labels: () => ({}),
31
+ animDuration: 1500
32
+ })
33
+
34
+ const clicksContext = inject<Ref<{ current: number }>>('$$slidev-clicks-context', ref(defaultClicksContext))
35
+ const slidevContext = inject<{ nav: { currentSlideNo: number } }>('$$slidev-context', { nav: defaultNav })
36
+ const slidePage = inject<Ref<number>>('$$slidev-page', ref(-1))
37
+
38
+ const containerRef = ref<HTMLElement | null>(null)
39
+ const sliderPosition = ref(props.at === -1 ? props.target : 0) // start at 0 for auto/on-enter, at position for manual
40
+ const isDragging = ref(false)
41
+ const isAutoRegistered = ref(false)
42
+ let animFrameId: number | null = null
43
+ let hasAnimated = false
44
+
45
+ const isHorizontal = computed(() => props.direction === 'horizontal')
46
+
47
+ const clipPath = computed(() => {
48
+ if (isHorizontal.value) {
49
+ return `inset(0 ${100 - sliderPosition.value}% 0 0)`
50
+ }
51
+ return `inset(0 0 ${100 - sliderPosition.value}% 0)`
52
+ })
53
+
54
+ const sliderStyle = computed(() => {
55
+ if (isHorizontal.value) {
56
+ return { left: `${sliderPosition.value}%` }
57
+ }
58
+ return { top: `${sliderPosition.value}%` }
59
+ })
60
+
61
+ function getPositionFromEvent(e: MouseEvent | TouchEvent) {
62
+ if (!containerRef.value) return sliderPosition.value
63
+
64
+ const rect = containerRef.value.getBoundingClientRect()
65
+ const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
66
+ const clientY = 'touches' in e ? e.touches[0].clientY : e.clientY
67
+
68
+ if (isHorizontal.value) {
69
+ const x = clientX - rect.left
70
+ return Math.max(0, Math.min(100, (x / rect.width) * 100))
71
+ }
72
+
73
+ const y = clientY - rect.top
74
+ return Math.max(0, Math.min(100, (y / rect.height) * 100))
75
+ }
76
+
77
+ function handleStart(e: MouseEvent | TouchEvent) {
78
+ isDragging.value = true
79
+ sliderPosition.value = getPositionFromEvent(e)
80
+ }
81
+
82
+ function handleMove(e: MouseEvent | TouchEvent) {
83
+ if (!isDragging.value) return
84
+ sliderPosition.value = getPositionFromEvent(e)
85
+ }
86
+
87
+ function handleEnd() {
88
+ isDragging.value = false
89
+ }
90
+
91
+ function handleKeydown(e: KeyboardEvent) {
92
+ const step = e.shiftKey ? 10 : 1
93
+
94
+ if (isHorizontal.value) {
95
+ if (e.key === 'ArrowLeft') {
96
+ sliderPosition.value = Math.max(0, sliderPosition.value - step)
97
+ e.preventDefault()
98
+ } else if (e.key === 'ArrowRight') {
99
+ sliderPosition.value = Math.min(100, sliderPosition.value + step)
100
+ e.preventDefault()
101
+ }
102
+ } else {
103
+ if (e.key === 'ArrowUp') {
104
+ sliderPosition.value = Math.max(0, sliderPosition.value - step)
105
+ e.preventDefault()
106
+ } else if (e.key === 'ArrowDown') {
107
+ sliderPosition.value = Math.min(100, sliderPosition.value + step)
108
+ e.preventDefault()
109
+ }
110
+ }
111
+ }
112
+
113
+ function animateSlider() {
114
+ if (animFrameId) cancelAnimationFrame(animFrameId)
115
+
116
+ const startPos = 0
117
+ const endPos = props.target
118
+ const startTime = performance.now()
119
+ const dur = props.animDuration
120
+ const ease = (t: number) => t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2 // easeInOutCubic
121
+
122
+ function step(now: number) {
123
+ const elapsed = now - startTime
124
+ const progress = Math.min(elapsed / dur, 1)
125
+ sliderPosition.value = startPos + (endPos - startPos) * ease(progress)
126
+
127
+ if (progress < 1) {
128
+ animFrameId = requestAnimationFrame(step)
129
+ } else {
130
+ sliderPosition.value = endPos
131
+ animFrameId = null
132
+ }
133
+ }
134
+
135
+ requestAnimationFrame(step)
136
+ }
137
+
138
+ function resetSlider() {
139
+ if (animFrameId) {
140
+ cancelAnimationFrame(animFrameId)
141
+ animFrameId = null
142
+ }
143
+ sliderPosition.value = 0
144
+ }
145
+
146
+ // Slidev keeps all slides in DOM. Detect slide activation via nav watcher.
147
+ onMounted(() => {
148
+ window.addEventListener('mousemove', handleMove)
149
+ window.addEventListener('mouseup', handleEnd)
150
+ window.addEventListener('touchmove', handleMove)
151
+ window.addEventListener('touchend', handleEnd)
152
+
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 && containerRef.value) {
157
+ const info = ctx.calculate('+1')
158
+ if (info) {
159
+ ctx.register(containerRef.value, info)
160
+ isAutoRegistered.value = true
161
+
162
+ const isActiveRef = info.isActive
163
+
164
+ watchEffect(() => {
165
+ const active = isActiveRef.value
166
+ if (active && !hasAnimated) {
167
+ hasAnimated = true
168
+ animateSlider()
169
+ } else if (!active && hasAnimated) {
170
+ hasAnimated = false
171
+ resetSlider()
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
+ hasAnimated = true
182
+ animateSlider()
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
+ hasAnimated = false
196
+ resetSlider()
197
+ if (props.at === 0) {
198
+ hasAnimated = true
199
+ animateSlider()
200
+ }
201
+ } else if (!isActive && wasActive) {
202
+ if (animFrameId) cancelAnimationFrame(animFrameId)
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 && !hasAnimated) {
212
+ hasAnimated = true
213
+ animateSlider()
214
+ } else if (cur < props.at && hasAnimated) {
215
+ hasAnimated = false
216
+ resetSlider()
217
+ }
218
+ })
219
+
220
+ onUnmounted(() => {
221
+ window.removeEventListener('mousemove', handleMove)
222
+ window.removeEventListener('mouseup', handleEnd)
223
+ window.removeEventListener('touchmove', handleMove)
224
+ window.removeEventListener('touchend', handleEnd)
225
+ if (animFrameId) cancelAnimationFrame(animFrameId)
226
+ if (isAutoRegistered.value && clicksContext.value?.unregister && containerRef.value) {
227
+ clicksContext.value.unregister(containerRef.value)
228
+ }
229
+ })
230
+ </script>
231
+
232
+ <template>
233
+ <div
234
+ ref="containerRef"
235
+ class="image-compare"
236
+ :class="[
237
+ `image-compare-${direction}`,
238
+ { 'image-compare-dragging': isDragging }
239
+ ]"
240
+ :style="{ height: height || undefined }"
241
+ @mousedown="handleStart"
242
+ @touchstart="handleStart"
243
+ >
244
+ <!-- After image (bottom layer) -->
245
+ <div class="image-compare-after">
246
+ <img :src="after" :alt="labels.after || 'After'" />
247
+ <span v-if="labels.after" class="image-compare-label image-compare-label-after">
248
+ {{ labels.after }}
249
+ </span>
250
+ </div>
251
+
252
+ <!-- Before image (top layer with clip) -->
253
+ <div class="image-compare-before" :style="{ clipPath }">
254
+ <img :src="before" :alt="labels.before || 'Before'" />
255
+ <span v-if="labels.before" class="image-compare-label image-compare-label-before">
256
+ {{ labels.before }}
257
+ </span>
258
+ </div>
259
+
260
+ <!-- Slider handle -->
261
+ <div
262
+ class="image-compare-slider"
263
+ :style="sliderStyle"
264
+ role="slider"
265
+ :aria-valuenow="sliderPosition"
266
+ aria-valuemin="0"
267
+ aria-valuemax="100"
268
+ tabindex="0"
269
+ @keydown="handleKeydown"
270
+ >
271
+ <div class="image-compare-handle">
272
+ <div class="image-compare-arrow image-compare-arrow-left">
273
+ <svg viewBox="0 0 24 24" fill="currentColor">
274
+ <path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
275
+ </svg>
276
+ </div>
277
+ <div class="image-compare-arrow image-compare-arrow-right">
278
+ <svg viewBox="0 0 24 24" fill="currentColor">
279
+ <path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z"/>
280
+ </svg>
281
+ </div>
282
+ </div>
283
+ </div>
284
+ </div>
285
+ </template>
286
+
287
+ <style>
288
+ .image-compare {
289
+ position: relative;
290
+ width: 100%;
291
+ overflow: hidden;
292
+ cursor: ew-resize;
293
+ user-select: none;
294
+ border-radius: 0.5rem;
295
+ }
296
+
297
+ .image-compare-vertical {
298
+ cursor: ns-resize;
299
+ }
300
+
301
+ .image-compare-dragging {
302
+ cursor: grabbing;
303
+ }
304
+
305
+ .image-compare-before,
306
+ .image-compare-after {
307
+ position: absolute;
308
+ inset: 0;
309
+ }
310
+
311
+ .image-compare-after {
312
+ z-index: 1;
313
+ }
314
+
315
+ .image-compare-before {
316
+ z-index: 2;
317
+ }
318
+
319
+ .image-compare-before img,
320
+ .image-compare-after img {
321
+ width: 100%;
322
+ height: 100%;
323
+ object-fit: cover;
324
+ display: block;
325
+ }
326
+
327
+ /* Labels */
328
+ .image-compare-label {
329
+ position: absolute;
330
+ padding: var(--space-xs) var(--space-sm);
331
+ font-size: var(--font-size-small);
332
+ font-weight: var(--font-weight-medium);
333
+ background-color: color-mix(in srgb, var(--color-black) 60%, transparent);
334
+ color: var(--color-drac-fg-50);
335
+ border-radius: 0.25rem;
336
+ z-index: 10;
337
+ }
338
+
339
+ .image-compare-horizontal .image-compare-label-before {
340
+ top: var(--space-sm);
341
+ left: var(--space-sm);
342
+ }
343
+
344
+ .image-compare-horizontal .image-compare-label-after {
345
+ top: var(--space-sm);
346
+ right: var(--space-sm);
347
+ }
348
+
349
+ .image-compare-vertical .image-compare-label-before {
350
+ top: var(--space-sm);
351
+ left: var(--space-sm);
352
+ }
353
+
354
+ .image-compare-vertical .image-compare-label-after {
355
+ bottom: var(--space-sm);
356
+ left: var(--space-sm);
357
+ }
358
+
359
+ /* Slider line */
360
+ .image-compare-slider {
361
+ position: absolute;
362
+ z-index: 3;
363
+ display: flex;
364
+ align-items: center;
365
+ justify-content: center;
366
+ }
367
+
368
+ .image-compare-horizontal .image-compare-slider {
369
+ top: 0;
370
+ bottom: 0;
371
+ width: 4px;
372
+ margin-left: -2px;
373
+ background-color: var(--color-drac-fg-50);
374
+ box-shadow: 0 0 4px color-mix(in srgb, var(--color-black) 30%, transparent);
375
+ }
376
+
377
+ .image-compare-vertical .image-compare-slider {
378
+ left: 0;
379
+ right: 0;
380
+ height: 4px;
381
+ margin-top: -2px;
382
+ background-color: var(--color-drac-fg-50);
383
+ box-shadow: 0 0 4px color-mix(in srgb, var(--color-black) 30%, transparent);
384
+ }
385
+
386
+ /* Handle */
387
+ .image-compare-handle {
388
+ position: absolute;
389
+ width: 44px;
390
+ height: 44px;
391
+ background-color: var(--color-drac-fg-50);
392
+ border-radius: 50%;
393
+ box-shadow: 0 2px 8px color-mix(in srgb, var(--color-black) 30%, transparent);
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: center;
397
+ }
398
+
399
+ .image-compare-arrow {
400
+ width: 16px;
401
+ height: 16px;
402
+ color: var(--color-text);
403
+ }
404
+
405
+ .image-compare-horizontal .image-compare-arrow-left {
406
+ margin-right: -2px;
407
+ }
408
+
409
+ .image-compare-horizontal .image-compare-arrow-right {
410
+ margin-left: -2px;
411
+ }
412
+
413
+ .image-compare-vertical .image-compare-handle {
414
+ flex-direction: column;
415
+ }
416
+
417
+ .image-compare-vertical .image-compare-arrow-left {
418
+ transform: rotate(90deg);
419
+ margin-bottom: -2px;
420
+ }
421
+
422
+ .image-compare-vertical .image-compare-arrow-right {
423
+ transform: rotate(90deg);
424
+ margin-top: -2px;
425
+ }
426
+
427
+ /* Focus styles */
428
+ .image-compare-slider:focus {
429
+ outline: 3px solid var(--color-primary);
430
+ outline-offset: 2px;
431
+ }
432
+
433
+ .image-compare-slider:focus .image-compare-handle {
434
+ border: 2px solid var(--color-primary);
435
+ }
436
+ </style>
@@ -0,0 +1,85 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * MatrixGrid widget — 2x2 grid for SWOT, priority matrices, etc.
4
+ *
5
+ * Generic usage:
6
+ * <MatrixGrid :cells="[
7
+ * { title: 'Quick Wins', items: ['Fix bug', 'Update docs'] },
8
+ * { title: 'Major Projects', items: ['New API'] },
9
+ * { title: 'Fill-Ins', items: ['Cleanup'] },
10
+ * { title: 'Thankless', items: ['Migration'] }
11
+ * ]" />
12
+ *
13
+ * SWOT usage:
14
+ * <MatrixGrid :cells="[
15
+ * { title: 'Strengths', items: [...], color: 'success' },
16
+ * { title: 'Opportunities', items: [...], color: 'primary' },
17
+ * { title: 'Weaknesses', items: [...], color: 'danger' },
18
+ * { title: 'Threats', items: [...], color: 'warning' }
19
+ * ]" />
20
+ */
21
+
22
+ import type { SemanticColor } from '../composables/useColors'
23
+ import { semanticColorVar } from '../composables/useColors'
24
+
25
+ defineProps<{
26
+ cells: Array<{
27
+ title: string
28
+ items: string[]
29
+ color?: SemanticColor
30
+ }>
31
+ }>()
32
+ </script>
33
+
34
+ <template>
35
+ <div class="matrix-grid">
36
+ <div
37
+ v-for="(cell, i) in cells"
38
+ :key="i"
39
+ class="matrix-cell"
40
+ >
41
+ <h3
42
+ class="matrix-cell-title"
43
+ :style="cell.color ? { color: semanticColorVar[cell.color] } : {}"
44
+ >{{ cell.title }}</h3>
45
+ <ul class="matrix-cell-list">
46
+ <li v-for="(item, j) in cell.items" :key="j">{{ item }}</li>
47
+ </ul>
48
+ </div>
49
+ </div>
50
+ </template>
51
+
52
+ <style scoped>
53
+ .matrix-grid {
54
+ display: grid;
55
+ grid-template-columns: 1fr 1fr;
56
+ gap: var(--space-sm);
57
+ margin-top: var(--space-lg);
58
+ }
59
+
60
+ .matrix-cell {
61
+ border: 1px solid var(--color-border);
62
+ border-radius: 0.5rem;
63
+ padding: var(--space-xs);
64
+ }
65
+
66
+ .matrix-cell-title {
67
+ background: var(--color-bg-soft);
68
+ padding: var(--space-xs) var(--space-sm);
69
+ border-radius: 0.25rem;
70
+ margin: 0 0 var(--space-xs);
71
+ font-size: var(--font-size-base);
72
+ font-weight: var(--font-weight-semibold);
73
+ }
74
+
75
+ .matrix-cell-list {
76
+ list-style: disc;
77
+ margin: 0;
78
+ padding-left: 1.5em;
79
+ }
80
+
81
+ .matrix-cell-list li {
82
+ margin-block: 0.25em;
83
+ font-size: var(--font-size-small);
84
+ }
85
+ </style>