@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,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, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
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">=></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>
|