@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,229 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CodeDiff - before/after code comparison using <pre> blocks.
4
+ *
5
+ * <CodeDiff mode="split">
6
+ * <pre filename="utils.js" lang="javascript">
7
+ * function add(a, b) {
8
+ * return a + b;
9
+ * }
10
+ * </pre>
11
+ * <pre filename="utils.ts" lang="typescript">
12
+ * function add(a: number, b: number): number {
13
+ * return a + b;
14
+ * }
15
+ * </pre>
16
+ * </CodeDiff>
17
+ */
18
+
19
+ import { computed, useSlots } from 'vue'
20
+ import type { VNode } from 'vue'
21
+ import type { CodeLine } from '../composables/useColors'
22
+ import { useShiki } from '../composables/useShiki'
23
+ import CodeHighlight from './CodeHighlight.vue'
24
+
25
+ const props = withDefaults(defineProps<{
26
+ mode?: 'split' | 'unified'
27
+ showLineNumbers?: boolean
28
+ highlightChanges?: boolean
29
+ }>(), {
30
+ mode: 'split',
31
+ showLineNumbers: true,
32
+ highlightChanges: true,
33
+ })
34
+
35
+ const slots = useSlots()
36
+ const { highlighter, tokenizeCode } = useShiki()
37
+
38
+ // --- Extract code from <pre> slot children ---
39
+
40
+ interface CodeBlock {
41
+ code: string
42
+ filename?: string
43
+ lang?: string
44
+ }
45
+
46
+ function extractText(vnode: VNode): string {
47
+ // Block elements (e.g. <p>) inserted by markdown-it when blank lines appear
48
+ // inside <pre>: restore the blank line separator with \n prefix
49
+ const prefix = typeof vnode.type === 'string' ? '\n' : ''
50
+ if (typeof vnode.children === 'string') return prefix + vnode.children
51
+ if (Array.isArray(vnode.children)) {
52
+ return prefix + (vnode.children as VNode[]).map(extractText).join('')
53
+ }
54
+ return ''
55
+ }
56
+
57
+ function dedent(text: string): string {
58
+ const lines = text.split('\n')
59
+ while (lines.length && lines[0].trim() === '') lines.shift()
60
+ while (lines.length && lines[lines.length - 1].trim() === '') lines.pop()
61
+ const minIndent = lines
62
+ .filter(l => l.trim().length > 0)
63
+ .reduce((min, l) => Math.min(min, (l.match(/^(\s*)/)?.[1].length ?? 0)), Infinity)
64
+ if (minIndent === Infinity || minIndent === 0) return lines.join('\n')
65
+ return lines.map(l => l.slice(minIndent)).join('\n')
66
+ }
67
+
68
+ const blocks = computed<CodeBlock[]>(() => {
69
+ if (!slots.default) return []
70
+ const vnodes = slots.default()
71
+ const result: CodeBlock[] = []
72
+
73
+ for (const vnode of vnodes) {
74
+ if (vnode.type === 'pre') {
75
+ const attrs = (vnode.props || {}) as Record<string, string>
76
+ result.push({
77
+ code: dedent(extractText(vnode)),
78
+ filename: attrs.filename,
79
+ lang: attrs.lang,
80
+ })
81
+ }
82
+ }
83
+
84
+ return result
85
+ })
86
+
87
+ const oldBlock = computed(() => blocks.value[0])
88
+ const newBlock = computed(() => blocks.value[1])
89
+
90
+ // --- Shiki pre-tokenization (full code blocks for proper multi-line context) ---
91
+
92
+ const oldTokens = computed<string[] | null>(() => {
93
+ if (!highlighter.value || !oldBlock.value?.lang) return null
94
+ return tokenizeCode(oldBlock.value.code, oldBlock.value.lang)
95
+ })
96
+
97
+ const newTokens = computed<string[] | null>(() => {
98
+ if (!highlighter.value || !newBlock.value?.lang) return null
99
+ return tokenizeCode(newBlock.value.code, newBlock.value.lang)
100
+ })
101
+
102
+ // --- LCS-based diff ---
103
+
104
+ function computeLCS(a: string[], b: string[]): string[] {
105
+ const m = a.length, n = b.length
106
+ const dp: number[][] = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0))
107
+ for (let i = 1; i <= m; i++)
108
+ for (let j = 1; j <= n; j++)
109
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1])
110
+ const lcs: string[] = []
111
+ let i = m, j = n
112
+ while (i > 0 && j > 0) {
113
+ if (a[i - 1] === b[j - 1]) { lcs.unshift(a[i - 1]); i--; j-- }
114
+ else if (dp[i - 1][j] > dp[i][j - 1]) i--
115
+ else j--
116
+ }
117
+ return lcs
118
+ }
119
+
120
+ interface DiffEntry {
121
+ type: 'unchanged' | 'removed' | 'added'
122
+ beforeLine?: string
123
+ afterLine?: string
124
+ beforeNum?: number
125
+ afterNum?: number
126
+ }
127
+
128
+ const diffResult = computed<DiffEntry[]>(() => {
129
+ if (!oldBlock.value || !newBlock.value) return []
130
+ const beforeLines = oldBlock.value.code.split('\n')
131
+ const afterLines = newBlock.value.code.split('\n')
132
+ const lcs = computeLCS(beforeLines, afterLines)
133
+ const result: DiffEntry[] = []
134
+
135
+ let bi = 0, ai = 0, li = 0
136
+ while (bi < beforeLines.length || ai < afterLines.length) {
137
+ if (li < lcs.length && beforeLines[bi] === lcs[li] && afterLines[ai] === lcs[li]) {
138
+ result.push({ type: 'unchanged', beforeLine: beforeLines[bi], afterLine: afterLines[ai], beforeNum: bi + 1, afterNum: ai + 1 })
139
+ bi++; ai++; li++
140
+ } else if (bi < beforeLines.length && (li >= lcs.length || beforeLines[bi] !== lcs[li])) {
141
+ result.push({ type: 'removed', beforeLine: beforeLines[bi], beforeNum: bi + 1 })
142
+ bi++
143
+ } else if (ai < afterLines.length && (li >= lcs.length || afterLines[ai] !== lcs[li])) {
144
+ result.push({ type: 'added', afterLine: afterLines[ai], afterNum: ai + 1 })
145
+ ai++
146
+ }
147
+ }
148
+ return result
149
+ })
150
+
151
+ const hl = computed(() => props.highlightChanges)
152
+
153
+ const beforeDiffLines = computed<CodeLine[]>(() =>
154
+ diffResult.value
155
+ .filter(d => d.type !== 'added')
156
+ .map(d => ({
157
+ text: d.beforeLine ?? '',
158
+ html: d.beforeNum != null ? oldTokens.value?.[d.beforeNum - 1] : undefined,
159
+ num: d.beforeNum,
160
+ type: hl.value ? d.type : 'unchanged' as const,
161
+ prefix: d.type === 'removed' ? '-' : ' ',
162
+ }))
163
+ )
164
+
165
+ const afterDiffLines = computed<CodeLine[]>(() =>
166
+ diffResult.value
167
+ .filter(d => d.type !== 'removed')
168
+ .map(d => ({
169
+ text: d.afterLine ?? '',
170
+ html: d.afterNum != null ? newTokens.value?.[d.afterNum - 1] : undefined,
171
+ num: d.afterNum,
172
+ type: hl.value ? d.type : 'unchanged' as const,
173
+ prefix: d.type === 'added' ? '+' : ' ',
174
+ }))
175
+ )
176
+ </script>
177
+
178
+ <template>
179
+ <div class="code-diff" :class="`code-diff-${mode}`">
180
+ <CodeHighlight
181
+ v-if="oldBlock"
182
+ :filename="oldBlock.filename"
183
+ :lang="oldBlock.lang"
184
+ :lines="beforeDiffLines"
185
+ :showLineNumbers="showLineNumbers"
186
+ />
187
+ <CodeHighlight
188
+ v-if="newBlock"
189
+ :filename="newBlock.filename"
190
+ :lang="newBlock.lang"
191
+ :lines="afterDiffLines"
192
+ :showLineNumbers="showLineNumbers"
193
+ />
194
+ </div>
195
+ </template>
196
+
197
+ <style>
198
+ .code-diff {
199
+ display: flex;
200
+ border-radius: 0.5rem;
201
+ overflow: hidden;
202
+ border: 1px solid var(--color-border);
203
+ }
204
+
205
+ .code-diff-split {
206
+ flex-direction: row;
207
+ }
208
+
209
+ .code-diff-unified {
210
+ flex-direction: column;
211
+ }
212
+
213
+ /* Children take equal width */
214
+ .code-diff > .code-hl {
215
+ flex: 1;
216
+ min-width: 0;
217
+ }
218
+
219
+ /* Remove inner CodeHighlight borders - parent handles framing */
220
+ .code-diff > .code-hl {
221
+ border: none;
222
+ border-radius: 0;
223
+ }
224
+
225
+ /* Divider between panels */
226
+ .code-diff-split > .code-hl:first-child {
227
+ border-right: 1px solid var(--color-border-strong);
228
+ }
229
+ </style>
@@ -0,0 +1,337 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CodeHighlight - code block with Shiki syntax highlighting, line numbers,
4
+ * filename header, line highlighting.
5
+ *
6
+ * Via prop:
7
+ * <CodeHighlight code="const x = 1;" lang="typescript" />
8
+ *
9
+ * Via <pre> slot (preserves whitespace in Slidev markdown):
10
+ * <CodeHighlight filename="app.ts" lang="typescript"><pre>
11
+ * const x: number = 1;
12
+ * const y: number = 2;
13
+ * </pre></CodeHighlight>
14
+ *
15
+ * Via lines prop (used internally by CodeDiff):
16
+ * <CodeHighlight :lines="computedLines" :showLineNumbers="true" lang="ts" />
17
+ */
18
+
19
+ import { ref, computed, useSlots, watch } from 'vue'
20
+ import type { VNode } from 'vue'
21
+ import type { SemanticColor, CodeLine } from '../composables/useColors'
22
+ import { useShiki } from '../composables/useShiki'
23
+
24
+ const props = withDefaults(defineProps<{
25
+ code?: string
26
+ lines?: CodeLine[]
27
+ filename?: string
28
+ lang?: string
29
+ showLineNumbers?: boolean
30
+ startLine?: number
31
+ highlights?: number[]
32
+ highlightColor?: SemanticColor
33
+ }>(), {
34
+ startLine: 1,
35
+ showLineNumbers: true,
36
+ highlightColor: 'primary',
37
+ })
38
+
39
+ const slots = useSlots()
40
+ const rootEl = ref<HTMLElement | null>(null)
41
+ const { highlighter, tokenizeCode } = useShiki()
42
+
43
+ // --- Raw code: prop > slot > empty ---
44
+
45
+ function extractSlotText(vnodes: VNode[]): string {
46
+ return vnodes.map(vnode => {
47
+ // Block elements (e.g. <p>) inserted by markdown-it when blank lines appear
48
+ // inside <pre>: restore the blank line separator with \n prefix
49
+ const prefix = typeof vnode.type === 'string' ? '\n' : ''
50
+ if (typeof vnode.children === 'string') return prefix + vnode.children
51
+ if (Array.isArray(vnode.children)) return prefix + extractSlotText(vnode.children as VNode[])
52
+ return ''
53
+ }).join('')
54
+ }
55
+
56
+ function dedent(text: string): string {
57
+ const lines = text.split('\n')
58
+ while (lines.length && lines[0].trim() === '') lines.shift()
59
+ while (lines.length && lines[lines.length - 1].trim() === '') lines.pop()
60
+ const minIndent = lines
61
+ .filter(l => l.trim().length > 0)
62
+ .reduce((min, l) => Math.min(min, (l.match(/^(\s*)/)?.[1].length ?? 0)), Infinity)
63
+ if (minIndent === Infinity || minIndent === 0) return lines.join('\n')
64
+ return lines.map(l => l.slice(minIndent)).join('\n')
65
+ }
66
+
67
+ const rawCode = computed(() => {
68
+ if (props.code) return props.code
69
+ if (slots.default) return dedent(extractSlotText(slots.default()))
70
+ return ''
71
+ })
72
+
73
+ // --- Shiki highlighting (reactive: re-runs when highlighter loads or code changes) ---
74
+
75
+ const shikiLines = computed<string[] | null>(() => {
76
+ if (!highlighter.value) return null
77
+ if (props.lines) return null // lines with html come pre-tokenized from CodeDiff
78
+ if (!rawCode.value || !props.lang) return null
79
+ return tokenizeCode(rawCode.value, props.lang)
80
+ })
81
+
82
+ // --- Resolved lines ---
83
+
84
+ const resolvedLines = computed<CodeLine[]>(() => {
85
+ if (props.lines) return props.lines
86
+ if (!rawCode.value) return []
87
+ const htmlLines = shikiLines.value
88
+ return rawCode.value.split('\n').map((text, i) => ({
89
+ text,
90
+ html: htmlLines?.[i],
91
+ num: props.startLine + i,
92
+ type: 'unchanged' as const,
93
+ }))
94
+ })
95
+
96
+ const highlightSet = computed(() => new Set(props.highlights ?? []))
97
+
98
+ // --- Fallback regex tokenizer (used while Shiki loads or for unknown langs) ---
99
+
100
+ function esc(s: string): string {
101
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
102
+ }
103
+
104
+ const KEYWORDS = new Set([
105
+ 'function', 'return', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 'do',
106
+ 'switch', 'case', 'break', 'continue', 'new', 'delete', 'typeof', 'instanceof',
107
+ 'class', 'extends', 'implements', 'interface', 'type', 'enum', 'import', 'export',
108
+ 'from', 'default', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'yield',
109
+ 'in', 'of', 'as', 'is', 'keyof', 'readonly', 'declare', 'abstract',
110
+ 'public', 'private', 'protected', 'static', 'override', 'get', 'set',
111
+ ])
112
+ const LITERALS = new Set([
113
+ 'number', 'string', 'boolean', 'void', 'null', 'undefined', 'any', 'never',
114
+ 'unknown', 'object', 'symbol', 'bigint', 'true', 'false', 'this', 'super',
115
+ ])
116
+
117
+ function wordClass(word: string): string | null {
118
+ if (KEYWORDS.has(word)) return 'hl-keyword'
119
+ if (LITERALS.has(word)) return 'hl-constant'
120
+ return null
121
+ }
122
+
123
+ function tokenize(text: string): string {
124
+ if (!props.lang || props.lang === 'text') return esc(text)
125
+
126
+ let result = ''
127
+ let i = 0
128
+
129
+ while (i < text.length) {
130
+ if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
131
+ const q = text[i]
132
+ let j = i + 1
133
+ while (j < text.length && text[j] !== q) { if (text[j] === '\\') j++; j++ }
134
+ j++
135
+ result += `<span class="hl-string">${esc(text.slice(i, j))}</span>`
136
+ i = j
137
+ continue
138
+ }
139
+ if (text[i] === '/' && text[i + 1] === '/') {
140
+ result += `<span class="hl-comment">${esc(text.slice(i))}</span>`
141
+ break
142
+ }
143
+ if (text[i] === '/' && text[i + 1] === '*') {
144
+ const end = text.indexOf('*/', i + 2)
145
+ const j = end >= 0 ? end + 2 : text.length
146
+ result += `<span class="hl-comment">${esc(text.slice(i, j))}</span>`
147
+ i = j
148
+ continue
149
+ }
150
+ if (/[a-zA-Z_$]/.test(text[i])) {
151
+ let j = i
152
+ while (j < text.length && /[a-zA-Z0-9_$]/.test(text[j])) j++
153
+ const word = text.slice(i, j)
154
+ const cls = wordClass(word)
155
+ result += cls ? `<span class="${cls}">${esc(word)}</span>` : esc(word)
156
+ i = j
157
+ continue
158
+ }
159
+ if (/\d/.test(text[i])) {
160
+ let j = i
161
+ while (j < text.length && /[\d.xXa-fA-F_]/.test(text[j])) j++
162
+ result += `<span class="hl-constant">${esc(text.slice(i, j))}</span>`
163
+ i = j
164
+ continue
165
+ }
166
+ if (text[i] === '=' && text[i + 1] === '>') {
167
+ result += '<span class="hl-keyword">=&gt;</span>'
168
+ i += 2
169
+ continue
170
+ }
171
+ if (/[{}()\[\];:,=<>+\-*\/&|!?.@#%^~]/.test(text[i])) {
172
+ result += `<span class="hl-punctuation">${esc(text[i])}</span>`
173
+ i++
174
+ continue
175
+ }
176
+ result += esc(text[i])
177
+ i++
178
+ }
179
+
180
+ return result
181
+ }
182
+ </script>
183
+
184
+ <template>
185
+ <div ref="rootEl" class="code-hl">
186
+ <div v-if="$slots.header || filename || lang" class="code-hl-header">
187
+ <slot name="header">
188
+ <span v-if="filename" class="code-hl-filename">{{ filename }}</span>
189
+ <span v-if="lang" class="code-hl-lang">{{ lang }}</span>
190
+ </slot>
191
+ </div>
192
+ <pre class="code-hl-content"><code><template v-for="(line, idx) in resolvedLines" :key="idx"><span
193
+ class="code-hl-line"
194
+ :class="[
195
+ line.type && line.type !== 'unchanged' ? `code-hl-line-${line.type}` : '',
196
+ highlightSet.has(line.num ?? idx + startLine) ? `code-hl-line-highlight code-hl-highlight-${highlightColor}` : '',
197
+ ]"
198
+ ><span v-if="showLineNumbers" class="code-hl-linenum">{{ line.num ?? '' }}</span><span v-if="line.prefix !== undefined" class="code-hl-prefix" :class="line.type ? `code-hl-prefix-${line.type}` : ''">{{ line.prefix }}</span><span class="code-hl-text" v-html="line.html ?? tokenize(line.text)"></span>
199
+ </span></template></code></pre>
200
+ </div>
201
+ </template>
202
+
203
+ <style>
204
+ .code-hl {
205
+ font-family: var(--font-mono);
206
+ font-size: var(--font-size-small);
207
+ line-height: var(--line-height-code);
208
+ letter-spacing: normal;
209
+ word-spacing: normal;
210
+ border-radius: 0.5rem;
211
+ overflow: hidden;
212
+ border: 1px solid var(--color-border);
213
+ background-color: var(--shiki-color-background, var(--color-bg-muted));
214
+ }
215
+
216
+ /* Header */
217
+ .code-hl-header {
218
+ display: flex;
219
+ align-items: center;
220
+ justify-content: space-between;
221
+ padding: 0.25rem 0.75rem;
222
+ border-bottom: 1px solid var(--color-border);
223
+ background-color: var(--shiki-color-background, var(--color-bg-muted));
224
+ }
225
+
226
+ .code-hl-filename {
227
+ font-weight: var(--font-weight-medium);
228
+ color: var(--color-text);
229
+ }
230
+
231
+ .code-hl-lang {
232
+ font-size: 0.8em;
233
+ color: var(--color-text-tertiary);
234
+ text-transform: uppercase;
235
+ letter-spacing: 0.05em;
236
+ }
237
+
238
+ /* Content - reset .slidev-layout pre overrides */
239
+ .code-hl-content {
240
+ margin: 0;
241
+ padding: 0.25rem 0;
242
+ border-radius: 0;
243
+ overflow: auto;
244
+ }
245
+
246
+ .code-hl-content code {
247
+ display: block;
248
+ }
249
+
250
+ /* Line */
251
+ .code-hl-line {
252
+ display: flex;
253
+ min-height: 1.4em;
254
+ }
255
+
256
+ /* Line numbers */
257
+ .code-hl-linenum {
258
+ width: 2.5em;
259
+ text-align: right;
260
+ padding-right: 0.75em;
261
+ color: var(--color-text-tertiary);
262
+ user-select: none;
263
+ flex-shrink: 0;
264
+ }
265
+
266
+ /* Diff prefix (+/-/space) */
267
+ .code-hl-prefix {
268
+ width: 1.5em;
269
+ text-align: center;
270
+ flex-shrink: 0;
271
+ user-select: none;
272
+ }
273
+
274
+ .code-hl-prefix-removed {
275
+ color: var(--color-danger);
276
+ font-weight: var(--font-weight-bold);
277
+ }
278
+
279
+ .code-hl-prefix-added {
280
+ color: var(--color-success);
281
+ font-weight: var(--font-weight-bold);
282
+ }
283
+
284
+ .code-hl-prefix-modified {
285
+ color: var(--color-warning);
286
+ font-weight: var(--font-weight-bold);
287
+ }
288
+
289
+ /* Code text */
290
+ .code-hl-text {
291
+ flex: 1;
292
+ white-space: pre-wrap;
293
+ overflow-wrap: break-word;
294
+ color: var(--shiki-color-text, var(--color-text));
295
+ }
296
+
297
+ /* Diff line backgrounds */
298
+ .code-hl-line-removed {
299
+ background-color: var(--color-danger-tint);
300
+ }
301
+
302
+ .code-hl-line-added {
303
+ background-color: var(--color-success-tint);
304
+ }
305
+
306
+ .code-hl-line-modified {
307
+ background-color: var(--color-warning-tint);
308
+ }
309
+
310
+ /* Highlight backgrounds */
311
+ .code-hl-line-highlight.code-hl-highlight-primary {
312
+ background-color: var(--color-primary-tint);
313
+ }
314
+
315
+ .code-hl-line-highlight.code-hl-highlight-success {
316
+ background-color: var(--color-success-tint);
317
+ }
318
+
319
+ .code-hl-line-highlight.code-hl-highlight-warning {
320
+ background-color: var(--color-warning-tint);
321
+ }
322
+
323
+ .code-hl-line-highlight.code-hl-highlight-danger {
324
+ background-color: var(--color-danger-tint);
325
+ }
326
+
327
+ .code-hl-line-highlight.code-hl-highlight-info {
328
+ background-color: var(--color-info-tint);
329
+ }
330
+
331
+ /* Fallback syntax tokens (used while Shiki loads) */
332
+ .hl-keyword { color: var(--shiki-token-keyword); }
333
+ .hl-string { color: var(--shiki-token-string); }
334
+ .hl-comment { color: var(--shiki-token-comment); font-style: italic; }
335
+ .hl-constant { color: var(--shiki-token-constant); }
336
+ .hl-punctuation { color: var(--shiki-token-punctuation); }
337
+ </style>
@@ -0,0 +1,114 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ColorSwatch primitive - displays a color variable with its resolved value.
4
+ *
5
+ * Automatically reads the actual CSS variable value at runtime.
6
+ * No hex props needed - resolves from --color-{name} directly.
7
+ *
8
+ * Usage:
9
+ * <ColorSwatch name="primary" />
10
+ * <ColorSwatch name="bg" />
11
+ * <ColorSwatch name="success" />
12
+ */
13
+
14
+ import { ref, computed, onMounted, watch } from 'vue'
15
+ import { useIsDark } from '../composables/useColors'
16
+
17
+ const props = defineProps<{
18
+ name: string
19
+ }>()
20
+
21
+ const isDark = useIsDark()
22
+ const resolvedColor = ref('')
23
+
24
+ function getCssVarName(): string {
25
+ return `--color-${props.name}`
26
+ }
27
+
28
+ function parseRgb(raw: string): [number, number, number] | null {
29
+ raw = raw.trim()
30
+ if (raw.startsWith('#')) {
31
+ const hex = raw.length === 4
32
+ ? raw[1] + raw[1] + raw[2] + raw[2] + raw[3] + raw[3]
33
+ : raw.slice(1, 7)
34
+ return [parseInt(hex.slice(0, 2), 16), parseInt(hex.slice(2, 4), 16), parseInt(hex.slice(4, 6), 16)]
35
+ }
36
+ const m = raw.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/)
37
+ if (m) return [+m[1], +m[2], +m[3]]
38
+ return null
39
+ }
40
+
41
+ function relativeLuminance(r: number, g: number, b: number): number {
42
+ const [rs, gs, bs] = [r / 255, g / 255, b / 255].map(c =>
43
+ c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4),
44
+ )
45
+ return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs
46
+ }
47
+
48
+ const isLightBg = computed(() => {
49
+ const rgb = parseRgb(resolvedColor.value)
50
+ if (!rgb) return false
51
+ return relativeLuminance(...rgb) > 0.2
52
+ })
53
+
54
+ function resolveColor() {
55
+ if (typeof document === 'undefined') return
56
+ const value = getComputedStyle(document.documentElement)
57
+ .getPropertyValue(getCssVarName())
58
+ .trim()
59
+ resolvedColor.value = value || '?'
60
+ }
61
+
62
+ onMounted(resolveColor)
63
+ watch(() => [isDark.value, props.name], resolveColor)
64
+ </script>
65
+
66
+ <template>
67
+ <div
68
+ class="color-swatch"
69
+ :class="{ 'color-swatch-light': isLightBg }"
70
+ :style="{ backgroundColor: `var(${getCssVarName()})` }"
71
+ >
72
+ <div class="color-swatch-label">
73
+ {{ getCssVarName() }}
74
+ <span class="color-swatch-hex">{{ resolvedColor }}</span>
75
+ </div>
76
+ </div>
77
+ </template>
78
+
79
+ <style>
80
+ .color-swatch {
81
+ height: 5rem;
82
+ border-radius: 0.5rem;
83
+ display: flex;
84
+ align-items: flex-end;
85
+ padding: 0.5rem;
86
+ border: 1px solid var(--color-border);
87
+ color: var(--color-white);
88
+ }
89
+
90
+ .color-swatch:has(.color-swatch-label) {
91
+ position: relative;
92
+ }
93
+
94
+ .color-swatch-label {
95
+ font-size: var(--font-size-small);
96
+ font-family: var(--font-mono);
97
+ line-height: 1.3;
98
+ text-shadow: 0 1px 3px color-mix(in srgb, var(--color-black) 30%, transparent);
99
+ }
100
+
101
+ .color-swatch-hex {
102
+ display: block;
103
+ font-size: 0.75rem;
104
+ opacity: 0.8;
105
+ }
106
+
107
+ .color-swatch-light {
108
+ color: var(--color-drac-fg-900);
109
+ }
110
+
111
+ .color-swatch-light .color-swatch-label {
112
+ text-shadow: 0 1px 3px color-mix(in srgb, var(--color-white) 40%, transparent);
113
+ }
114
+ </style>