@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,364 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * CodeHighlight - code block with syntax highlighting, line numbers, filename header.
4
+ *
5
+ * Standalone:
6
+ * <CodeHighlight code="const x = 1;" lang="typescript" />
7
+ * <CodeHighlight filename="app.ts" lang="typescript">
8
+ * const x: number = 1;
9
+ * </CodeHighlight>
10
+ *
11
+ * Inside CodeDiff (inherits diff lines + showLineNumbers):
12
+ * <CodeDiff mode="split">
13
+ * <template #before>
14
+ * <CodeHighlight filename="old.js" lang="javascript">...</CodeHighlight>
15
+ * </template>
16
+ * <template #after>
17
+ * <CodeHighlight filename="new.ts" lang="typescript">...</CodeHighlight>
18
+ * </template>
19
+ * </CodeDiff>
20
+ */
21
+
22
+ import { ref, computed, inject, onMounted, useSlots } from 'vue'
23
+ import type { Ref, VNode } from 'vue'
24
+ import type { SemanticColor, CodeLine } from '../composables/useColors'
25
+
26
+ interface CodeDiffContext {
27
+ register(code: string): number // returns index (0=before, 1=after)
28
+ getDiffLines(index: number): Ref<CodeLine[]>
29
+ showLineNumbers: Ref<boolean>
30
+ }
31
+
32
+ const props = withDefaults(defineProps<{
33
+ code?: string
34
+ lines?: CodeLine[]
35
+ filename?: string
36
+ lang?: string
37
+ showLineNumbers?: boolean // undefined = inherit from CodeDiff or default true
38
+ startLine?: number
39
+ highlights?: number[]
40
+ highlightColor?: SemanticColor
41
+ }>(), {
42
+ startLine: 1,
43
+ highlightColor: 'primary',
44
+ })
45
+
46
+ const slots = useSlots()
47
+ const rootEl = ref<HTMLElement | null>(null)
48
+
49
+ // --- Raw code: prop > slot > empty ---
50
+
51
+ function extractSlotText(vnodes: VNode[]): string {
52
+ return vnodes.map(vnode => {
53
+ if (typeof vnode.children === 'string') return vnode.children
54
+ if (Array.isArray(vnode.children)) return extractSlotText(vnode.children as VNode[])
55
+ return ''
56
+ }).join('')
57
+ }
58
+
59
+ function dedent(text: string): string {
60
+ const lines = text.split('\n')
61
+ while (lines.length && lines[0].trim() === '') lines.shift()
62
+ while (lines.length && lines[lines.length - 1].trim() === '') lines.pop()
63
+ const minIndent = lines
64
+ .filter(l => l.trim().length > 0)
65
+ .reduce((min, l) => Math.min(min, (l.match(/^(\s*)/)?.[1].length ?? 0)), Infinity)
66
+ if (minIndent === Infinity || minIndent === 0) return lines.join('\n')
67
+ return lines.map(l => l.slice(minIndent)).join('\n')
68
+ }
69
+
70
+ const rawCode = computed(() => {
71
+ if (props.code) return props.code
72
+ if (slots.default) return dedent(extractSlotText(slots.default()))
73
+ return ''
74
+ })
75
+
76
+ // --- CodeDiff integration ---
77
+
78
+ const diffContext = inject<CodeDiffContext | null>('codeDiffContext', null)
79
+ const myIndex = ref(-1)
80
+
81
+ onMounted(() => {
82
+ console.log('[CodeHighlight] onMounted', { diffContext: !!diffContext, rawCode: rawCode.value?.substring(0, 50), filename: props.filename })
83
+ if (diffContext) {
84
+ myIndex.value = diffContext.register(rawCode.value)
85
+ console.log('[CodeHighlight] registered as index', myIndex.value)
86
+ }
87
+ })
88
+
89
+ const diffLines = computed(() => {
90
+ if (diffContext && myIndex.value >= 0) return diffContext.getDiffLines(myIndex.value).value
91
+ return null
92
+ })
93
+
94
+ // --- Resolved lines ---
95
+
96
+ const resolvedLines = computed<CodeLine[]>(() => {
97
+ if (diffLines.value) return diffLines.value
98
+ if (props.lines) return props.lines
99
+ if (!rawCode.value) return []
100
+ return rawCode.value.split('\n').map((text, i) => ({
101
+ text,
102
+ num: props.startLine + i,
103
+ type: 'unchanged' as const,
104
+ }))
105
+ })
106
+
107
+ const resolvedShowLineNumbers = computed(() => {
108
+ if (props.showLineNumbers !== undefined) return props.showLineNumbers
109
+ if (diffContext) return diffContext.showLineNumbers.value
110
+ return true
111
+ })
112
+
113
+ const highlightSet = computed(() => new Set(props.highlights ?? []))
114
+
115
+ // --- Syntax tokenizer ---
116
+
117
+ const KEYWORDS = new Set([
118
+ 'function', 'return', 'const', 'let', 'var', 'if', 'else', 'for', 'while', 'do',
119
+ 'switch', 'case', 'break', 'continue', 'new', 'delete', 'typeof', 'instanceof',
120
+ 'class', 'extends', 'implements', 'interface', 'type', 'enum', 'import', 'export',
121
+ 'from', 'default', 'async', 'await', 'try', 'catch', 'finally', 'throw', 'yield',
122
+ 'in', 'of', 'as', 'is', 'keyof', 'readonly', 'declare', 'abstract',
123
+ 'public', 'private', 'protected', 'static', 'override', 'get', 'set',
124
+ ])
125
+ const LITERALS = new Set([
126
+ 'number', 'string', 'boolean', 'void', 'null', 'undefined', 'any', 'never',
127
+ 'unknown', 'object', 'symbol', 'bigint', 'true', 'false', 'this', 'super',
128
+ ])
129
+
130
+ function wordClass(word: string): string | null {
131
+ if (KEYWORDS.has(word)) return 'hl-keyword'
132
+ if (LITERALS.has(word)) return 'hl-constant'
133
+ return null
134
+ }
135
+
136
+ function esc(s: string): string {
137
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
138
+ }
139
+
140
+ function tokenize(text: string): string {
141
+ if (!props.lang || props.lang === 'text') return esc(text)
142
+
143
+ let result = ''
144
+ let i = 0
145
+
146
+ while (i < text.length) {
147
+ // Strings
148
+ if (text[i] === '"' || text[i] === "'" || text[i] === '`') {
149
+ const q = text[i]
150
+ let j = i + 1
151
+ while (j < text.length && text[j] !== q) { if (text[j] === '\\') j++; j++ }
152
+ j++
153
+ result += `<span class="hl-string">${esc(text.slice(i, j))}</span>`
154
+ i = j
155
+ continue
156
+ }
157
+
158
+ // Line comments
159
+ if (text[i] === '/' && text[i + 1] === '/') {
160
+ result += `<span class="hl-comment">${esc(text.slice(i))}</span>`
161
+ break
162
+ }
163
+
164
+ // Block comments
165
+ if (text[i] === '/' && text[i + 1] === '*') {
166
+ const end = text.indexOf('*/', i + 2)
167
+ const j = end >= 0 ? end + 2 : text.length
168
+ result += `<span class="hl-comment">${esc(text.slice(i, j))}</span>`
169
+ i = j
170
+ continue
171
+ }
172
+
173
+ // Words
174
+ if (/[a-zA-Z_$]/.test(text[i])) {
175
+ let j = i
176
+ while (j < text.length && /[a-zA-Z0-9_$]/.test(text[j])) j++
177
+ const word = text.slice(i, j)
178
+ const cls = wordClass(word)
179
+ result += cls ? `<span class="${cls}">${esc(word)}</span>` : esc(word)
180
+ i = j
181
+ continue
182
+ }
183
+
184
+ // Numbers
185
+ if (/\d/.test(text[i])) {
186
+ let j = i
187
+ while (j < text.length && /[\d.xXa-fA-F_]/.test(text[j])) j++
188
+ result += `<span class="hl-constant">${esc(text.slice(i, j))}</span>`
189
+ i = j
190
+ continue
191
+ }
192
+
193
+ // Arrow =>
194
+ if (text[i] === '=' && text[i + 1] === '>') {
195
+ result += '<span class="hl-keyword">=&gt;</span>'
196
+ i += 2
197
+ continue
198
+ }
199
+
200
+ // Punctuation
201
+ if (/[{}()\[\];:,=<>+\-*\/&|!?.@#%^~]/.test(text[i])) {
202
+ result += `<span class="hl-punctuation">${esc(text[i])}</span>`
203
+ i++
204
+ continue
205
+ }
206
+
207
+ result += esc(text[i])
208
+ i++
209
+ }
210
+
211
+ return result
212
+ }
213
+ </script>
214
+
215
+ <template>
216
+ <div ref="rootEl" class="code-hl">
217
+ <div v-if="$slots.header || filename || lang" class="code-hl-header">
218
+ <slot name="header">
219
+ <span v-if="filename" class="code-hl-filename">{{ filename }}</span>
220
+ <span v-if="lang" class="code-hl-lang">{{ lang }}</span>
221
+ </slot>
222
+ </div>
223
+ <pre class="code-hl-content"><code><template v-for="(line, idx) in resolvedLines" :key="idx"><span
224
+ class="code-hl-line"
225
+ :class="[
226
+ line.type && line.type !== 'unchanged' ? `code-hl-line-${line.type}` : '',
227
+ highlightSet.has(line.num ?? idx + startLine) ? `code-hl-line-highlight code-hl-highlight-${highlightColor}` : '',
228
+ ]"
229
+ ><span v-if="resolvedShowLineNumbers" 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="tokenize(line.text)"></span>
230
+ </span></template></code></pre>
231
+ </div>
232
+ </template>
233
+
234
+ <style>
235
+ .code-hl {
236
+ font-family: var(--font-mono);
237
+ font-size: var(--font-size-small);
238
+ line-height: var(--line-height-code);
239
+ border-radius: 0.5rem;
240
+ overflow: hidden;
241
+ border: 1px solid var(--color-border);
242
+ background-color: var(--color-bg-muted);
243
+ }
244
+
245
+ /* Header */
246
+ .code-hl-header {
247
+ display: flex;
248
+ align-items: center;
249
+ justify-content: space-between;
250
+ padding: var(--space-xs) var(--space-sm);
251
+ border-bottom: 1px solid var(--color-border);
252
+ background-color: var(--color-bg-muted);
253
+ }
254
+
255
+ .code-hl-filename {
256
+ font-weight: var(--font-weight-medium);
257
+ color: var(--color-text);
258
+ }
259
+
260
+ .code-hl-lang {
261
+ font-size: 0.75em;
262
+ color: var(--color-text-tertiary);
263
+ text-transform: uppercase;
264
+ letter-spacing: 0.05em;
265
+ }
266
+
267
+ /* Content */
268
+ .code-hl-content {
269
+ margin: 0;
270
+ padding: var(--space-xs) 0;
271
+ overflow: auto;
272
+ }
273
+
274
+ .code-hl-content code {
275
+ display: block;
276
+ }
277
+
278
+ /* Line */
279
+ .code-hl-line {
280
+ display: flex;
281
+ min-height: 1.4em;
282
+ }
283
+
284
+ /* Line numbers */
285
+ .code-hl-linenum {
286
+ width: 2.5em;
287
+ text-align: right;
288
+ padding-right: 0.75em;
289
+ color: var(--color-text-tertiary);
290
+ user-select: none;
291
+ flex-shrink: 0;
292
+ }
293
+
294
+ /* Diff prefix (+/-/space) */
295
+ .code-hl-prefix {
296
+ width: 1.5em;
297
+ text-align: center;
298
+ flex-shrink: 0;
299
+ user-select: none;
300
+ }
301
+
302
+ .code-hl-prefix-removed {
303
+ color: var(--color-danger);
304
+ font-weight: var(--font-weight-bold);
305
+ }
306
+
307
+ .code-hl-prefix-added {
308
+ color: var(--color-success);
309
+ font-weight: var(--font-weight-bold);
310
+ }
311
+
312
+ .code-hl-prefix-modified {
313
+ color: var(--color-warning);
314
+ font-weight: var(--font-weight-bold);
315
+ }
316
+
317
+ /* Code text */
318
+ .code-hl-text {
319
+ flex: 1;
320
+ white-space: pre;
321
+ color: var(--shiki-color-text, var(--color-text));
322
+ }
323
+
324
+ /* Diff line backgrounds */
325
+ .code-hl-line-removed {
326
+ background-color: var(--color-danger-tint);
327
+ }
328
+
329
+ .code-hl-line-added {
330
+ background-color: var(--color-success-tint);
331
+ }
332
+
333
+ .code-hl-line-modified {
334
+ background-color: var(--color-warning-tint);
335
+ }
336
+
337
+ /* Highlight backgrounds */
338
+ .code-hl-line-highlight.code-hl-highlight-primary {
339
+ background-color: var(--color-primary-tint);
340
+ }
341
+
342
+ .code-hl-line-highlight.code-hl-highlight-success {
343
+ background-color: var(--color-success-tint);
344
+ }
345
+
346
+ .code-hl-line-highlight.code-hl-highlight-warning {
347
+ background-color: var(--color-warning-tint);
348
+ }
349
+
350
+ .code-hl-line-highlight.code-hl-highlight-danger {
351
+ background-color: var(--color-danger-tint);
352
+ }
353
+
354
+ .code-hl-line-highlight.code-hl-highlight-info {
355
+ background-color: var(--color-info-tint);
356
+ }
357
+
358
+ /* Syntax highlighting tokens (mapped to Shiki CSS vars) */
359
+ .hl-keyword { color: var(--shiki-token-keyword); }
360
+ .hl-string { color: var(--shiki-token-string); }
361
+ .hl-comment { color: var(--shiki-token-comment); font-style: italic; }
362
+ .hl-constant { color: var(--shiki-token-constant); }
363
+ .hl-punctuation { color: var(--shiki-token-punctuation); }
364
+ </style>
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Shared color type system — single source of truth for all color-related
3
+ * types and CSS variable mappings used across components.
4
+ */
5
+
6
+ import { ref, onMounted } from 'vue'
7
+
8
+ // === READ-ONLY DARK MODE DETECTION ===
9
+
10
+ /**
11
+ * Observes the `dark` class on <html> without writing to it.
12
+ * Unlike VueUse's useDark(), this does NOT modify the DOM
13
+ * and won't conflict with Slidev's colorSchema setting.
14
+ */
15
+ export function useIsDark() {
16
+ const isDark = ref(false)
17
+
18
+ function update() {
19
+ if (typeof document !== 'undefined') {
20
+ isDark.value = document.documentElement.classList.contains('dark')
21
+ }
22
+ }
23
+
24
+ onMounted(() => {
25
+ update()
26
+ const observer = new MutationObserver(update)
27
+ observer.observe(document.documentElement, {
28
+ attributes: true,
29
+ attributeFilter: ['class'],
30
+ })
31
+ })
32
+
33
+ return isDark
34
+ }
35
+
36
+ // === TYPE DEFINITIONS ===
37
+
38
+ /** Core 5 semantic colors (have background + foreground pairs) */
39
+ export type SemanticColor = 'primary' | 'success' | 'warning' | 'danger' | 'info'
40
+
41
+ /** Extended with secondary/accent */
42
+ export type ExtendedColor = SemanticColor | 'secondary' | 'accent'
43
+
44
+ /** For text/icon components that also accept 'inherit' */
45
+ export type ColorWithInherit = 'inherit' | SemanticColor
46
+
47
+ /** Full text palette (text components can use text-* vars too) */
48
+ export type TextColor = 'inherit' | 'secondary' | 'tertiary' | 'accent' | SemanticColor
49
+
50
+ /** Background semantic colors (includes non-semantic bg variants) */
51
+ export type BgColor = SemanticColor | 'soft' | 'muted'
52
+
53
+ // === CODE LINE TYPE (for CodeHighlight / CodeDiff) ===
54
+
55
+ export interface CodeLine {
56
+ text: string
57
+ html?: string
58
+ num?: number
59
+ type?: 'added' | 'removed' | 'modified' | 'unchanged'
60
+ prefix?: string
61
+ }
62
+
63
+ // === CSS VAR MAPS ===
64
+
65
+ export const semanticColorVar: Record<SemanticColor, string> = {
66
+ primary: 'var(--color-primary)',
67
+ success: 'var(--color-success)',
68
+ warning: 'var(--color-warning)',
69
+ danger: 'var(--color-danger)',
70
+ info: 'var(--color-info)',
71
+ }
72
+
73
+ export const semanticForegroundVar: Record<SemanticColor, string> = {
74
+ primary: 'var(--color-primary-foreground)',
75
+ success: 'var(--color-success-foreground)',
76
+ warning: 'var(--color-warning-foreground)',
77
+ danger: 'var(--color-danger-foreground)',
78
+ info: 'var(--color-info-foreground)',
79
+ }
80
+
81
+ export const gradientVar: Record<ExtendedColor, string> = {
82
+ primary: 'var(--gradient-primary)',
83
+ success: 'var(--gradient-success)',
84
+ warning: 'var(--gradient-warning)',
85
+ danger: 'var(--gradient-danger)',
86
+ info: 'var(--gradient-info)',
87
+ accent: 'var(--gradient-accent)',
88
+ secondary: 'var(--gradient-primary)',
89
+ }
90
+
91
+ export const textColorVar: Record<TextColor, string> = {
92
+ inherit: 'inherit',
93
+ secondary: 'var(--color-text-secondary)',
94
+ tertiary: 'var(--color-text-tertiary)',
95
+ accent: 'var(--color-accent)',
96
+ primary: 'var(--color-primary)',
97
+ success: 'var(--color-success)',
98
+ warning: 'var(--color-warning)',
99
+ danger: 'var(--color-danger)',
100
+ info: 'var(--color-info)',
101
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * useShiki - singleton Shiki highlighter for CodeHighlight / CodeDiff.
3
+ *
4
+ * Uses the same CSS-variables theme as setup/shiki.ts, so tokens
5
+ * auto-adapt to light/dark via --shiki-token-* vars from colors.css.
6
+ * JS regex engine (no WASM) for fast, lightweight initialization.
7
+ */
8
+
9
+ import { shallowRef } from 'vue'
10
+ import { createHighlighterCore, createCssVariablesTheme } from 'shiki/core'
11
+ import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
12
+ import type { HighlighterCore } from 'shiki/core'
13
+
14
+ const theme = createCssVariablesTheme({
15
+ name: 'troshab',
16
+ variablePrefix: '--shiki-',
17
+ variableDefaults: {},
18
+ fontStyle: true,
19
+ })
20
+
21
+ let initPromise: Promise<HighlighterCore> | null = null
22
+
23
+ async function init(): Promise<HighlighterCore> {
24
+ return createHighlighterCore({
25
+ themes: [theme],
26
+ langs: [
27
+ import('shiki/langs/javascript.mjs'),
28
+ import('shiki/langs/typescript.mjs'),
29
+ import('shiki/langs/html.mjs'),
30
+ import('shiki/langs/css.mjs'),
31
+ import('shiki/langs/vue.mjs'),
32
+ import('shiki/langs/json.mjs'),
33
+ import('shiki/langs/yaml.mjs'),
34
+ import('shiki/langs/bash.mjs'),
35
+ import('shiki/langs/shell.mjs'),
36
+ import('shiki/langs/python.mjs'),
37
+ import('shiki/langs/markdown.mjs'),
38
+ import('shiki/langs/sql.mjs'),
39
+ import('shiki/langs/go.mjs'),
40
+ import('shiki/langs/rust.mjs'),
41
+ ],
42
+ engine: createJavaScriptRegexEngine(),
43
+ })
44
+ }
45
+
46
+ function esc(s: string): string {
47
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
48
+ }
49
+
50
+ /**
51
+ * Returns a shared Shiki highlighter instance.
52
+ * Initialization is async; `tokenizeCode` returns null until ready.
53
+ */
54
+ export function useShiki() {
55
+ const highlighter = shallowRef<HighlighterCore | null>(null)
56
+
57
+ if (!initPromise) initPromise = init()
58
+ initPromise.then(hl => { highlighter.value = hl }).catch(() => {})
59
+
60
+ /**
61
+ * Tokenize a full code block. Returns per-line HTML strings,
62
+ * or null if Shiki isn't ready / lang unsupported.
63
+ */
64
+ function tokenizeCode(code: string, lang: string): string[] | null {
65
+ const hl = highlighter.value
66
+ if (!hl || !lang || lang === 'text') return null
67
+ try {
68
+ const result = hl.codeToTokens(code, { lang, theme: 'troshab' })
69
+ return result.tokens.map(line =>
70
+ line.map(t => {
71
+ const escaped = esc(t.content)
72
+ return t.color ? `<span style="color:${t.color}">${escaped}</span>` : escaped
73
+ }).join('')
74
+ )
75
+ } catch {
76
+ return null
77
+ }
78
+ }
79
+
80
+ return { highlighter, tokenizeCode }
81
+ }