@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.
- package/CLAUDE.md +537 -0
- package/LICENSE +134 -0
- package/README.md +168 -0
- package/SKILL.md +414 -0
- package/components/AnimatedCounter.vue +35 -0
- package/components/Background.vue +204 -0
- package/components/Callout.vue +135 -0
- package/components/Card.vue +75 -0
- package/components/CardGrid.vue +67 -0
- package/components/CaseStudy.vue +66 -0
- package/components/CodeDiff.vue +229 -0
- package/components/CodeHighlight.vue +337 -0
- package/components/ColorSwatch.vue +114 -0
- package/components/Confetti.vue +292 -0
- package/components/Conversation.vue +405 -0
- package/components/Countdown.vue +476 -0
- package/components/Definition.vue +59 -0
- package/components/DeviceMockup.vue +392 -0
- package/components/Funnel.vue +87 -0
- package/components/Icon.vue +73 -0
- package/components/Iframe.vue +38 -0
- package/components/Image.vue +69 -0
- package/components/ImageCompare.vue +436 -0
- package/components/MatrixGrid.vue +85 -0
- package/components/MermaidChart.vue +299 -0
- package/components/Metric.vue +161 -0
- package/components/PersonCard.vue +165 -0
- package/components/PricingTable.vue +144 -0
- package/components/Progress.vue +100 -0
- package/components/Pyramid.vue +81 -0
- package/components/QRCode.vue +137 -0
- package/components/QuoteBlock.vue +101 -0
- package/components/SpeechBubble.vue +169 -0
- package/components/Stepper.vue +542 -0
- package/components/StyledList.vue +156 -0
- package/components/StyledText.vue +275 -0
- package/components/SwotGrid.vue +99 -0
- package/components/Tags.vue +20 -0
- package/components/Testimonial.vue +243 -0
- package/components/Typewriter.vue +181 -0
- package/components_base/AnimatedCounter.vue +208 -0
- package/components_base/CodeHighlight.vue +364 -0
- package/composables/useColors.ts +101 -0
- package/composables/useShiki.ts +81 -0
- package/example_content.md +371 -0
- package/example_dark.md +10 -0
- package/example_slides/001-cover.md +15 -0
- package/example_slides/002-agenda.md +25 -0
- package/example_slides/003-section-layouts.md +14 -0
- package/example_slides/004-fullscreen-centered.md +7 -0
- package/example_slides/005-fullscreen-align-bottom.md +14 -0
- package/example_slides/006-fullscreen-no-padding.md +14 -0
- package/example_slides/007-fullscreen-bg-image-dark.md +13 -0
- package/example_slides/008-fullscreen-bg-image-light.md +13 -0
- package/example_slides/009-fullscreen-bg-gradient.md +15 -0
- package/example_slides/010-fullscreen-bg-color.md +13 -0
- package/example_slides/011-split-basic.md +17 -0
- package/example_slides/012-split-image-text.md +18 -0
- package/example_slides/013-split-contrast.md +22 -0
- package/example_slides/014-columns-basic.md +13 -0
- package/example_slides/015-columns-two.md +26 -0
- package/example_slides/016-columns-ratios.md +22 -0
- package/example_slides/017-columns-three.md +31 -0
- package/example_slides/018-columns-four.md +22 -0
- package/example_slides/019-columns-alignment.md +23 -0
- package/example_slides/020-columns-styled.md +21 -0
- package/example_slides/021-footnote-prop.md +16 -0
- package/example_slides/022-iframe-fullscreen.md +8 -0
- package/example_slides/023-iframe-split.md +18 -0
- package/example_slides/024-section-components.md +14 -0
- package/example_slides/025-styled-text.md +9 -0
- package/example_slides/026-styled-text.md +15 -0
- package/example_slides/027-text-formatting.md +28 -0
- package/example_slides/028-text-spoiler.md +15 -0
- package/example_slides/029-icon-component.md +47 -0
- package/example_slides/030-metric-component.md +29 -0
- package/example_slides/031-person-card.md +33 -0
- package/example_slides/032-styled-list.md +50 -0
- package/example_slides/033-color-swatch.md +35 -0
- package/example_slides/034-code-highlight.md +9 -0
- package/example_slides/035-iframe-component.md +9 -0
- package/example_slides/036-callout.md +15 -0
- package/example_slides/037-card-grid.md +27 -0
- package/example_slides/038-stepper-variants.md +18 -0
- package/example_slides/039-stepper-clicks.md +49 -0
- package/example_slides/040-stepper-interactive.md +28 -0
- package/example_slides/041-tags-progress.md +21 -0
- package/example_slides/042-speech-bubble.md +30 -0
- package/example_slides/043-conversation.md +13 -0
- package/example_slides/044-device-iphone.md +26 -0
- package/example_slides/045-device-browser.md +7 -0
- package/example_slides/046-qrcode.md +26 -0
- package/example_slides/047-countdown.md +14 -0
- package/example_slides/048-typewriter.md +8 -0
- package/example_slides/049-confetti.md +16 -0
- package/example_slides/050-image-compare.md +13 -0
- package/example_slides/051-code-diff.md +24 -0
- package/example_slides/052-quote-block.md +8 -0
- package/example_slides/053-testimonial.md +26 -0
- package/example_slides/054-testimonial-featured.md +16 -0
- package/example_slides/055-funnel.md +12 -0
- package/example_slides/056-pyramid.md +13 -0
- package/example_slides/057-pricing-table.md +9 -0
- package/example_slides/058-swot-grid.md +12 -0
- package/example_slides/059-matrix-grid.md +12 -0
- package/example_slides/060-case-study.md +11 -0
- package/example_slides/061-definition.md +15 -0
- package/example_slides/062-mermaid-intro.md +34 -0
- package/example_slides/063-mermaid-flowchart.md +19 -0
- package/example_slides/064-mermaid-sequence.md +17 -0
- package/example_slides/065-mermaid-xy-chart.md +16 -0
- package/example_slides/066-mermaid-pie.md +17 -0
- package/example_slides/067-mermaid-class.md +19 -0
- package/example_slides/068-mermaid-state.md +19 -0
- package/example_slides/069-mermaid-er.md +22 -0
- package/example_slides/070-mermaid-gantt.md +24 -0
- package/example_slides/071-mermaid-timeline.md +17 -0
- package/example_slides/072-mermaid-mindmap.md +21 -0
- package/example_slides/073-mermaid-gitgraph.md +20 -0
- package/example_slides/074-mermaid-split.md +30 -0
- package/example_slides/075-mermaid-columns.md +32 -0
- package/example_slides/076-section-addons.md +14 -0
- package/example_slides/077-asciinema.md +27 -0
- package/example_slides/078-fancyarrow.md +31 -0
- package/example_slides/079-fancyarrow-demo.md +23 -0
- package/example_slides/080-section-theme.md +14 -0
- package/example_slides/081-color-architecture.md +22 -0
- package/example_slides/082-semantic-text-colors.md +25 -0
- package/example_slides/083-typography.md +16 -0
- package/example_slides/084-typography-rationale.md +22 -0
- package/example_slides/085-icons.md +24 -0
- package/example_slides/086-tables.md +14 -0
- package/example_slides/087-code-blocks.md +18 -0
- package/example_slides/088-motion-modes.md +35 -0
- package/example_slides/089-slide-transitions.md +31 -0
- package/example_slides/090-v-click-reveals.md +40 -0
- package/example_slides/091-accessibility.md +27 -0
- package/example_slides/092-safe-zone.md +17 -0
- package/example_slides/093-questions.md +8 -0
- package/example_white.md +10 -0
- package/fonts/IBMPlexMono-Medium.woff2 +1449 -0
- package/fonts/IBMPlexMono-Regular.woff2 +1449 -0
- package/fonts/IBMPlexSans-Bold.woff2 +1449 -0
- package/fonts/IBMPlexSans-Medium.woff2 +1449 -0
- package/fonts/IBMPlexSans-Regular.woff2 +1449 -0
- package/fonts/IBMPlexSans-SemiBold.woff2 +1449 -0
- package/fonts/LICENSE.txt +93 -0
- package/layouts/slide.vue +251 -0
- package/package.json +62 -0
- package/public/avatars/alice.png +0 -0
- package/public/avatars/bob.png +0 -0
- package/public/avatars/carol.png +0 -0
- package/scripts/chart-audit.mjs +216 -0
- package/scripts/contrast-audit.mjs +299 -0
- package/scripts/generate-palette.mjs +395 -0
- package/scripts/integrity-audit.mjs +357 -0
- package/scripts/shared/css-utils.mjs +216 -0
- package/scripts/shiki-audit.mjs +300 -0
- package/scripts/typography-audit.mjs +300 -0
- package/setup/main.ts +107 -0
- package/setup/mermaid.ts +237 -0
- package/setup/shiki.ts +40 -0
- package/snippets/demo.ts +26 -0
- package/styles/base.css +1053 -0
- package/styles/colors.css +422 -0
- package/styles/index.css +12 -0
- package/styles/motion.css +486 -0
- 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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
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">=></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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
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
|
+
}
|