slidev-theme-tud 0.0.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/.envrc +1 -0
- package/.github/workflows/deploy.yml +84 -0
- package/.github/workflows/release.yml +89 -0
- package/.vscode/extensions.json +3 -0
- package/README.md +60 -0
- package/assets/TUD-logo-bl.svg +1 -0
- package/assets/TUD-logo-no-color.svg +1 -0
- package/assets/TUD-logo-text-no-color.svg +1 -0
- package/assets/TUD-logo-text-small-no-color.svg +1 -0
- package/assets/TUD-logo-tr.svg +1 -0
- package/assets/favicons/apple-touch-icon.png +0 -0
- package/assets/favicons/dark/apple-touch-icon.png +0 -0
- package/assets/favicons/dark/favicon-16x16.png +0 -0
- package/assets/favicons/dark/favicon-32x32.png +0 -0
- package/assets/favicons/dark/favicon.ico +0 -0
- package/assets/favicons/favicon-16x16.png +0 -0
- package/assets/favicons/favicon-32x32.png +0 -0
- package/assets/favicons/favicon.ico +0 -0
- package/assets/fonts/FiraCode-VariableFont_wght.ttf +0 -0
- package/assets/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf +0 -0
- package/assets/fonts/NotoSans-VariableFont_wdth,wght.ttf +0 -0
- package/assets/fonts/OFL.txt +93 -0
- package/components/.gitkeep +0 -0
- package/components/Background.vue +130 -0
- package/components/Footnotes.vue +164 -0
- package/example.mdc +372 -0
- package/flake.lock +61 -0
- package/flake.nix +22 -0
- package/global-bottom.vue +79 -0
- package/global-top.vue +72 -0
- package/layouts/README.md +2 -0
- package/layouts/cols.vue +38 -0
- package/layouts/cover-blue.vue +60 -0
- package/layouts/cover-white.vue +58 -0
- package/layouts/cover.vue +10 -0
- package/layouts/default.vue +15 -0
- package/layouts/section-blue.vue +32 -0
- package/layouts/section-n.vue +171 -0
- package/layouts/section-white.vue +25 -0
- package/layouts/section.vue +10 -0
- package/markdown.ts +196 -0
- package/package.json +70 -0
- package/pnpm-workspace.yaml +23 -0
- package/scripts/background.ts +69 -0
- package/scripts/color.ts +81 -0
- package/scripts/util.ts +51 -0
- package/setup/katex.ts +35 -0
- package/setup/shiki.ts +48 -0
- package/shims.d.ts +27 -0
- package/styles/fade-transition.css +36 -0
- package/styles/font.css +28 -0
- package/styles/index.ts +5 -0
- package/styles/layout.css +184 -0
- package/tsconfig.json +30 -0
- package/uno.config.ts +35 -0
- package/vite.config.ts +29 -0
package/markdown.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import type { MarkdownExit } from 'markdown-exit'
|
|
2
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
3
|
+
import { parseSync } from '@slidev/parser'
|
|
4
|
+
|
|
5
|
+
// markdown customizations for the theme
|
|
6
|
+
// - footnotes teleported into a <Footnotes> component
|
|
7
|
+
// - figure captions (`{.caption}`)
|
|
8
|
+
// - table captions (a `{.caption}` paragraph after a table)
|
|
9
|
+
// - deck-wide "Figure N:" / "Table N:" numbering
|
|
10
|
+
|
|
11
|
+
// global figure/table numbering across the whole deck, constructed at build time
|
|
12
|
+
// we parse the full deck once (cached by mtime), count the numbered figures/tables
|
|
13
|
+
// on every earlier slide, and use those
|
|
14
|
+
// running totals as the starting offset for this slide.
|
|
15
|
+
|
|
16
|
+
interface DeckCounts { mtimeMs: number, fig: number[], tab: number[] }
|
|
17
|
+
const deckCountsCache = new Map<string, DeckCounts>()
|
|
18
|
+
|
|
19
|
+
// Count numbered figure and table captions
|
|
20
|
+
function countNumbered(md: MarkdownExit, content: string): { fig: number, tab: number } {
|
|
21
|
+
const tokens = md.parse(content, { __figTabCount: true })
|
|
22
|
+
let fig = 0, tab = 0
|
|
23
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
24
|
+
const token = tokens[i]
|
|
25
|
+
// Figures: count markdown images
|
|
26
|
+
if (token.type === 'inline') {
|
|
27
|
+
for (const child of token.children ?? []) {
|
|
28
|
+
if (child.type !== 'image') continue;
|
|
29
|
+
const classes = classesOf(child)
|
|
30
|
+
if (classes.includes('caption') && classes.includes('numbered'))
|
|
31
|
+
fig++
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Tables: a `{.caption .numbered}` paragraph directly after a table.
|
|
35
|
+
const tableClasses = tableCaptionClasses(tokens, i)
|
|
36
|
+
if (tableClasses?.includes('caption') && tableClasses.includes('numbered'))
|
|
37
|
+
tab++
|
|
38
|
+
}
|
|
39
|
+
return { fig, tab }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function deckCounts(md: MarkdownExit, entryPath: string): DeckCounts | null {
|
|
43
|
+
let stat
|
|
44
|
+
try { stat = statSync(entryPath) }
|
|
45
|
+
catch { return null }
|
|
46
|
+
const cached = deckCountsCache.get(entryPath)
|
|
47
|
+
if (cached && cached.mtimeMs === stat.mtimeMs)
|
|
48
|
+
return cached
|
|
49
|
+
const parsed = parseSync(readFileSync(entryPath, 'utf-8'), entryPath)
|
|
50
|
+
const counts: DeckCounts = { mtimeMs: stat.mtimeMs, fig: [], tab: [] }
|
|
51
|
+
for (const slide of parsed.slides) {
|
|
52
|
+
const { fig, tab } = countNumbered(md, slide.content ?? '')
|
|
53
|
+
counts.fig.push(fig)
|
|
54
|
+
counts.tab.push(tab)
|
|
55
|
+
}
|
|
56
|
+
deckCountsCache.set(entryPath, counts)
|
|
57
|
+
return counts
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// How many numbered figures/tables appear on slides *before* the one rendering
|
|
61
|
+
// now — i.e. the offset this slide's numbering should start from.
|
|
62
|
+
function numberingOffset(md: MarkdownExit, env: any): { fig: number, tab: number } {
|
|
63
|
+
const id = /^(.*)__slidev_(\d+)\.md$/.exec(env?.id ?? '')
|
|
64
|
+
if (!id)
|
|
65
|
+
return { fig: 0, tab: 0 }
|
|
66
|
+
const counts = deckCounts(md, id[1])
|
|
67
|
+
if (!counts)
|
|
68
|
+
return { fig: 0, tab: 0 }
|
|
69
|
+
const slideIndex = Number(id[2]) - 1
|
|
70
|
+
let fig = 0
|
|
71
|
+
let tab = 0
|
|
72
|
+
for (let i = 0; i < slideIndex && i < counts.fig.length; i++) {
|
|
73
|
+
fig += counts.fig[i]
|
|
74
|
+
tab += counts.tab[i]
|
|
75
|
+
}
|
|
76
|
+
return { fig, tab }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
type Tok = { type: string, children?: Tok[] | null, attrs?: [string, string][] | null }
|
|
80
|
+
|
|
81
|
+
// Gather every class on a token parsed with comark
|
|
82
|
+
function classesOf(token: Tok): string[] {
|
|
83
|
+
return (token.attrs ?? [])
|
|
84
|
+
.filter(([name]) => name === 'class')
|
|
85
|
+
.flatMap(([, value]) => value.split(/\s+/))
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// If the token at `i` is a paragraph sitting directly after a table, return its
|
|
90
|
+
// classes; otherwise null. markdown-it always emits a paragraph as exactly
|
|
91
|
+
// [paragraph_open, inline, paragraph_close] — however rich the caption content
|
|
92
|
+
// is, it lives inside the `inline` token's children, so this 3-token shape (and
|
|
93
|
+
// thus the fixed offsets) always holds. The `{.caption}`/`{.numbered}` markers
|
|
94
|
+
// may land on the paragraph_open itself or on a props token among the inline
|
|
95
|
+
// children, so classes are gathered from both. Used by both the deck-wide
|
|
96
|
+
// counter and the caption rewrite so their detection can't drift apart.
|
|
97
|
+
// works with nested content as well
|
|
98
|
+
function tableCaptionClasses(tokens: Tok[], i: number): string[] | null {
|
|
99
|
+
if (tokens[i]?.type !== 'paragraph_close'
|
|
100
|
+
|| tokens[i - 1]?.type !== 'inline'
|
|
101
|
+
|| tokens[i - 2]?.type !== 'paragraph_open'
|
|
102
|
+
|| tokens[i - 3]?.type !== 'table_close')
|
|
103
|
+
return null
|
|
104
|
+
return [tokens[i - 2], ...(tokens[i - 1].children ?? [])].flatMap(classesOf)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Footnotes: render footnotes into a <Footnotes> component
|
|
108
|
+
// inline footnote refs (i.e. [^1] markers) are not changed
|
|
109
|
+
export function setupFootnotes(md: MarkdownExit): void {
|
|
110
|
+
md.renderer.rules.footnote_block_open = () =>
|
|
111
|
+
'<Footnotes><section class="footnotes"><ol class="footnotes-list">\n'
|
|
112
|
+
md.renderer.rules.footnote_block_close = () =>
|
|
113
|
+
'</ol></section></Footnotes>\n'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// images marked with `{.caption}` get their alt text rendered as caption.
|
|
117
|
+
// see `caption-*` in styles/layout.css.
|
|
118
|
+
export function setupFigureCaptions(md: MarkdownExit): void {
|
|
119
|
+
const defaultImage = md.renderer.rules.image
|
|
120
|
+
?? ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options))
|
|
121
|
+
md.renderer.rules.image = (tokens, idx, options, env, self) => {
|
|
122
|
+
const token = tokens[idx]
|
|
123
|
+
const img = defaultImage(tokens, idx, options, env, self)
|
|
124
|
+
const classes = classesOf(token)
|
|
125
|
+
if (!classes.includes('caption'))
|
|
126
|
+
return img
|
|
127
|
+
const alt = md.utils.escapeHtml(token.content ?? '')
|
|
128
|
+
let label = ''
|
|
129
|
+
if (classes.includes('numbered')) {
|
|
130
|
+
const e = env as { __figureNo?: number }
|
|
131
|
+
e.__figureNo ??= numberingOffset(md, env).fig
|
|
132
|
+
label = `<span class="caption-label">Figure ${++e.__figureNo}: </span>`
|
|
133
|
+
}
|
|
134
|
+
return `<span class="caption-figure">${img}<span class="caption-text">${label}${alt}</span></span>`
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// table analogue of the figure captions above
|
|
139
|
+
// see styles/layout.css
|
|
140
|
+
export function setupTableCaptions(md: MarkdownExit): void {
|
|
141
|
+
md.core.ruler.push('table_caption', (state) => {
|
|
142
|
+
// skip the rewrite on the counting pass (countNumbered)
|
|
143
|
+
if (state.env?.__figTabCount) return
|
|
144
|
+
const tokens = state.tokens
|
|
145
|
+
const Token = state.Token
|
|
146
|
+
|
|
147
|
+
// 1. in document order: match caption paragraphs directly after tables
|
|
148
|
+
const captions: { close: number, numbered: boolean, num?: number }[] = []
|
|
149
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
150
|
+
const classes = tableCaptionClasses(tokens, i)
|
|
151
|
+
if (!classes?.includes('caption')) continue
|
|
152
|
+
captions.push({ close: i, numbered: classes.includes('numbered') })
|
|
153
|
+
}
|
|
154
|
+
if (captions.length === 0) return
|
|
155
|
+
|
|
156
|
+
let tableNo = numberingOffset(md, state.env).tab
|
|
157
|
+
for (const caption of captions)
|
|
158
|
+
if (caption.numbered)
|
|
159
|
+
caption.num = ++tableNo
|
|
160
|
+
|
|
161
|
+
// 2. in reverse: rewrite the captions
|
|
162
|
+
for (let k = captions.length - 1; k >= 0; k--) {
|
|
163
|
+
const { close: i, numbered, num } = captions[k]
|
|
164
|
+
const pInline = tokens[i - 1]
|
|
165
|
+
const pOpen = tokens[i - 2]
|
|
166
|
+
const pClose = tokens[i]
|
|
167
|
+
|
|
168
|
+
// Locate the <table> this caption belongs to.
|
|
169
|
+
let open = i - 3
|
|
170
|
+
while (open >= 0 && tokens[open].type !== 'table_open') open--
|
|
171
|
+
if (open < 0) continue
|
|
172
|
+
|
|
173
|
+
// Retag the paragraph as the caption span (`.caption-text` gives it the
|
|
174
|
+
// same look as the figure captions).
|
|
175
|
+
pOpen.tag = 'span'
|
|
176
|
+
pOpen.block = false
|
|
177
|
+
pOpen.attrs = null
|
|
178
|
+
pOpen.attrSet('class', 'caption-text')
|
|
179
|
+
pClose.tag = 'span'
|
|
180
|
+
pClose.block = false
|
|
181
|
+
|
|
182
|
+
if (numbered) {
|
|
183
|
+
const label = new Token('html_inline', '', 0)
|
|
184
|
+
label.content = `<span class="caption-label">Table ${num}: </span>`
|
|
185
|
+
;(pInline.children ??= []).unshift(label)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const wrapOpen = new Token('html_block', '', 0)
|
|
189
|
+
wrapOpen.content = '<div class="caption-table">\n'
|
|
190
|
+
const wrapClose = new Token('html_block', '', 0)
|
|
191
|
+
wrapClose.content = '</div>\n'
|
|
192
|
+
tokens.splice(i + 1, 0, wrapClose)
|
|
193
|
+
tokens.splice(open, 0, wrapOpen)
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "slidev-theme-tud",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Corporate barkhausen Institut theme for SliDev",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"slidev-theme",
|
|
8
|
+
"slidev",
|
|
9
|
+
"TUD",
|
|
10
|
+
"Dresden University of Technology"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/maxkurze1/slidev-theme-TUD.git"
|
|
15
|
+
},
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/maxkurze1/slidev-theme-TUD/issues"
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"dayjs": "^1.11.20",
|
|
27
|
+
"gsap": "^3.13.0",
|
|
28
|
+
"katex": "^0.16.33",
|
|
29
|
+
"stylus": "^0.64.0",
|
|
30
|
+
"two.js": "^0.8.23"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"slidev-component-progress": "*"
|
|
34
|
+
},
|
|
35
|
+
"peerDependenciesMeta": {
|
|
36
|
+
"slidev-component-progress": {
|
|
37
|
+
"optional": true
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@slidev/cli": "^52.1.0",
|
|
42
|
+
"@slidev/types": "^52.1.0",
|
|
43
|
+
"slidev-component-progress": "github:maxkurze1/slidev-component-progress"
|
|
44
|
+
},
|
|
45
|
+
"slidev": {
|
|
46
|
+
"colorSchema": "light",
|
|
47
|
+
"defaults": {
|
|
48
|
+
"aspectRatio": "16/9",
|
|
49
|
+
"mdc": true,
|
|
50
|
+
"comark": true,
|
|
51
|
+
"view-transition-clicks": true,
|
|
52
|
+
"canvasWidth": 1280,
|
|
53
|
+
"presenter": true,
|
|
54
|
+
"titleTemplate": "%s",
|
|
55
|
+
"transition": "slide-fade-left | slide-fade-right",
|
|
56
|
+
"favicon": "",
|
|
57
|
+
"fonts": {
|
|
58
|
+
"sans": "NotoSans",
|
|
59
|
+
"mono": "FiraCode",
|
|
60
|
+
"provider": null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"scripts": {
|
|
65
|
+
"build": "slidev build example.mdc",
|
|
66
|
+
"dev": "slidev example.mdc --remote",
|
|
67
|
+
"export": "slidev export example.mdc",
|
|
68
|
+
"screenshot": "slidev export example.mdc --format png"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
overrides:
|
|
2
|
+
'@babel/core@<=7.29.0': '>=7.29.6'
|
|
3
|
+
dompurify@<3.3.2: '>=3.3.2'
|
|
4
|
+
dompurify@<3.4.0: '>=3.4.0'
|
|
5
|
+
dompurify@<3.4.7: '>=3.4.7'
|
|
6
|
+
dompurify@<3.4.9: '>=3.4.9'
|
|
7
|
+
dompurify@<=3.3.1: '>=3.3.2'
|
|
8
|
+
dompurify@<=3.3.3: '>=3.4.0'
|
|
9
|
+
dompurify@<=3.4.10: '>=3.4.11'
|
|
10
|
+
dompurify@<=3.4.5: '>=3.4.6'
|
|
11
|
+
dompurify@<=3.4.6: '>=3.4.7'
|
|
12
|
+
dompurify@>=1.0.10 <3.4.0: '>=3.4.0'
|
|
13
|
+
dompurify@>=3.0.0 <=3.4.7: '>=3.4.8'
|
|
14
|
+
dompurify@>=3.0.1 <3.4.0: '>=3.4.0'
|
|
15
|
+
dompurify@>=3.1.3 <=3.3.1: '>=3.3.2'
|
|
16
|
+
esbuild@>=0.27.3 <0.28.1: '>=0.28.1'
|
|
17
|
+
js-yaml@<3.15.0: '>=3.15.0'
|
|
18
|
+
js-yaml@>=4.0.0 <=4.1.1: '>=4.2.0'
|
|
19
|
+
linkify-it@<=5.0.0: '>=5.0.1'
|
|
20
|
+
markdown-it@<=14.1.1: '>=14.2.0'
|
|
21
|
+
mermaid@>=11.0.0-alpha.1 <=11.14.0: '>=11.15.0'
|
|
22
|
+
uuid@<11.1.1: '>=11.1.1'
|
|
23
|
+
vite@>=7.0.0 <=7.3.4: '>=7.3.5'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { reactive, toValue, watchEffect } from 'vue'
|
|
3
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
4
|
+
import { useSlideContext } from '@slidev/client'
|
|
5
|
+
import { toCssColor, toRawColor } from './color'
|
|
6
|
+
|
|
7
|
+
function createRegistry<T>() {
|
|
8
|
+
const store = reactive<Record<number, T>>({})
|
|
9
|
+
|
|
10
|
+
function publish(value: MaybeRefOrGetter<T>) {
|
|
11
|
+
const page = toValue(useSlideContext().$page)
|
|
12
|
+
watchEffect(() => {
|
|
13
|
+
store[page] = toValue(value)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { store, publish }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const backgroundsReg = createRegistry<string>()
|
|
21
|
+
export const slideBackgrounds = backgroundsReg.store
|
|
22
|
+
export function useBackground(color: MaybeRefOrGetter<string>) {
|
|
23
|
+
backgroundsReg.publish(() => toRawColor(toValue(color)))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
// A mark's placement. (x, y) is the slide-pixel position of the logo's center
|
|
28
|
+
export interface MarkTransform {
|
|
29
|
+
scale: number
|
|
30
|
+
x: number
|
|
31
|
+
y: number
|
|
32
|
+
rotate?: number // degrees
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface BackgroundLogo {
|
|
36
|
+
bl: MarkTransform
|
|
37
|
+
tr: MarkTransform
|
|
38
|
+
fill?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const bgLogosReg = createRegistry<BackgroundLogo>()
|
|
42
|
+
export const slideBgLogos = bgLogosReg.store
|
|
43
|
+
|
|
44
|
+
export function useBackgroundLogo(logo: MaybeRefOrGetter<BackgroundLogo>) {
|
|
45
|
+
bgLogosReg.publish(() => {
|
|
46
|
+
const l = toValue(logo)
|
|
47
|
+
return l.fill === undefined ? l : { ...l, fill: toRawColor(l.fill) }
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SlideLogos {
|
|
52
|
+
text?: string
|
|
53
|
+
normal?: string
|
|
54
|
+
small?: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const slideLogosReg = createRegistry<SlideLogos>()
|
|
58
|
+
export const slideLogos = slideLogosReg.store
|
|
59
|
+
|
|
60
|
+
export function useLogo(logos: MaybeRefOrGetter<SlideLogos>) {
|
|
61
|
+
slideLogosReg.publish(() => {
|
|
62
|
+
const l = toValue(logos)
|
|
63
|
+
const resolved: SlideLogos = {}
|
|
64
|
+
if (l.text !== undefined) resolved.text = toCssColor(l.text)
|
|
65
|
+
if (l.normal !== undefined) resolved.normal = toCssColor(l.normal)
|
|
66
|
+
if (l.small !== undefined) resolved.small = toCssColor(l.small)
|
|
67
|
+
return resolved
|
|
68
|
+
})
|
|
69
|
+
}
|
package/scripts/color.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// `colors` / `paletteNames` — the TU Dresden corporate palette, the single
|
|
2
|
+
// source of truth for the theme's colors (consumed by uno.config.ts, which
|
|
3
|
+
// registers them as UnoCSS theme colors and exposes them as `--theme-*`
|
|
4
|
+
// CSS vars).
|
|
5
|
+
|
|
6
|
+
export const colors = {
|
|
7
|
+
blue: { DEFAULT: '#2F57B2', 1: '#2F57B2', 2: '#97C6FF' },
|
|
8
|
+
violet: { DEFAULT: '#7369BE', 1: '#7369BE', 2: '#C8C8FF' },
|
|
9
|
+
magenta: { DEFAULT: '#BC1589', 1: '#BC1589', 2: '#FFB9FF' },
|
|
10
|
+
red: { DEFAULT: '#D20F41', 1: '#D20F41', 2: '#FFAAA5' },
|
|
11
|
+
orange: { DEFAULT: '#C85000', 1: '#C85000', 2: '#FFBE78' },
|
|
12
|
+
yellow: { DEFAULT: '#FFC700', 1: '#FFC700', 2: '#FFE483' },
|
|
13
|
+
olive: { DEFAULT: '#767A23', 1: '#767A23', 2: '#D2DC46' },
|
|
14
|
+
green: { DEFAULT: '#007D4B', 1: '#007D4B', 2: '#8CE6AA' },
|
|
15
|
+
teal: { DEFAULT: '#0A777F', 1: '#0A777F', 2: '#8CE6D7' },
|
|
16
|
+
primary: '#00008C',
|
|
17
|
+
darkblue: '#001450',
|
|
18
|
+
gray: '#323F4B',
|
|
19
|
+
} as const
|
|
20
|
+
|
|
21
|
+
// Every valid palette colour name
|
|
22
|
+
export const paletteNames: string[] = Object.entries(colors).flatMap(([name, value]) =>
|
|
23
|
+
typeof value === 'string'
|
|
24
|
+
? [name]
|
|
25
|
+
: [name, ...Object.keys(value)
|
|
26
|
+
.filter((shade) => shade !== 'DEFAULT')
|
|
27
|
+
.map((shade) => `${name}-${shade}`)],
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export const toCssColor = (name: string) => (new Set(paletteNames).has(name) ? `var(--theme-${name})` : name)
|
|
31
|
+
|
|
32
|
+
// Resolve a palette colour name to its concrete hex value; pass anything else through unchanged
|
|
33
|
+
export function toRawColor(name: string): string {
|
|
34
|
+
const [base, shade] = name.split('-')
|
|
35
|
+
const entry = (colors as Record<string, string | Record<string, string>>)[base]
|
|
36
|
+
if (typeof entry === 'string') return shade === undefined ? entry : name
|
|
37
|
+
if (entry !== undefined) return entry[shade ?? 'DEFAULT'] ?? name
|
|
38
|
+
return name
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Predefined color sets.
|
|
42
|
+
export const COLOR_COMBOS = [
|
|
43
|
+
{ bg: 'red-2', logo: 'magenta-2', text: 'primary' },
|
|
44
|
+
{ bg: 'violet', logo: 'magenta', text: 'white' },
|
|
45
|
+
{ bg: 'violet-2', logo: 'primary', text: 'white' },
|
|
46
|
+
{ bg: 'teal-1', logo: 'teal-2', text: 'primary' },
|
|
47
|
+
{ bg: 'yellow-2', logo: 'teal-2', text: 'primary' },
|
|
48
|
+
{ bg: 'magenta-2', logo: 'blue-2', text: 'primary' },
|
|
49
|
+
] as const
|
|
50
|
+
|
|
51
|
+
export function comboAt(i: number) {
|
|
52
|
+
const n = COLOR_COMBOS.length
|
|
53
|
+
return COLOR_COMBOS[(((Math.trunc(i) - 1) % n) + n) % n]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// The colour fields a combo provides; also the fields section-n resolves.
|
|
57
|
+
export type ComboField = keyof (typeof COLOR_COMBOS)[number]
|
|
58
|
+
|
|
59
|
+
// A colour spec as layouts pass it: a colour string (hex / CSS / palette name),
|
|
60
|
+
// a combo index (number, or an all-digits string), or omitted.
|
|
61
|
+
export type ColorSpec = number | string | undefined
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
export function asIndex(spec: ColorSpec): number | undefined {
|
|
65
|
+
if (typeof spec === 'number') return spec
|
|
66
|
+
if (typeof spec === 'string' && /^\d+$/.test(spec)) return Number(spec)
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// full resolve
|
|
71
|
+
export function resolveColorName(spec: ColorSpec, field: ComboField, defaultIndex: number): string {
|
|
72
|
+
const i = asIndex(spec)
|
|
73
|
+
if (i !== undefined) return comboAt(i)[field]
|
|
74
|
+
if (spec === undefined || spec === '') return comboAt(defaultIndex)[field]
|
|
75
|
+
return String(spec)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Resolve a spec straight to a CSS <color> — resolveColorName then toCssColor.
|
|
79
|
+
export function resolveCssColor(spec: ColorSpec, field: ComboField, defaultIndex: number): string {
|
|
80
|
+
return toCssColor(resolveColorName(spec, field, defaultIndex))
|
|
81
|
+
}
|
package/scripts/util.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Import dayjs's native ESM build (not the UMD `dayjs`/`dayjs/plugin/*` entries).
|
|
2
|
+
// The UMD bundle has no real `default` export, so when this theme is consumed as a
|
|
3
|
+
// dependency Vite serves it raw and `import dayjs from "dayjs"` throws
|
|
4
|
+
// "doesn't provide an export named: 'default'". The `/esm` build is proper ESM.
|
|
5
|
+
import dayjs from "dayjs/esm";
|
|
6
|
+
import advancedFormat from "dayjs/esm/plugin/advancedFormat";
|
|
7
|
+
import { inject, ref } from "vue";
|
|
8
|
+
import type { Ref } from "vue";
|
|
9
|
+
|
|
10
|
+
export const WIDTH = 1280
|
|
11
|
+
export const HEIGHT = 720
|
|
12
|
+
|
|
13
|
+
// `Do` (ordinal day, e.g. "23rd") lives in the advancedFormat plugin.
|
|
14
|
+
dayjs.extend(advancedFormat);
|
|
15
|
+
|
|
16
|
+
/* ================================ */
|
|
17
|
+
/* export composables */
|
|
18
|
+
/* ================================ */
|
|
19
|
+
|
|
20
|
+
// Slidev's authoritative, reactive slide scale.
|
|
21
|
+
//
|
|
22
|
+
// Caution this relies on Slidev's internal injection key.
|
|
23
|
+
// It seems there is no other way to access the slide's
|
|
24
|
+
// scale reactively.
|
|
25
|
+
export function useScale(): Ref<number> {
|
|
26
|
+
return inject<Ref<number>>("$$slidev-slide-scale", ref(1));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function formatString(template : string, values : any) {
|
|
30
|
+
return template.replace(/{(\w+)}/g, (_, key) => values[key] ?? `{${key}}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Expand `\today` and `\now` tokens in a string, each with an optional
|
|
34
|
+
// day.js format in square brackets (see https://day.js.org/docs/en/display/format):
|
|
35
|
+
// \today -> "June 23rd, 2026" (the long, human format)
|
|
36
|
+
// \today[YYYY-MM-DD] -> "2026-06-23"
|
|
37
|
+
// \now -> "2026-06-23 14:05:09" (date + time, no format given)
|
|
38
|
+
// \now[HH:mm] -> "14:05"
|
|
39
|
+
// `\today` and `\now` only differ in their default (no-bracket) format; both
|
|
40
|
+
// accept any day.js format string.
|
|
41
|
+
export function expandDateTokens(template : string, now = new Date()) {
|
|
42
|
+
const d = dayjs(now);
|
|
43
|
+
return String(template).replace(
|
|
44
|
+
/\\(today|now)(?:\[([^\]]*)\])?/g,
|
|
45
|
+
(_, kind, fmt) => {
|
|
46
|
+
if (fmt != null)
|
|
47
|
+
return d.format(fmt);
|
|
48
|
+
return kind === "today" ? d.format("MMMM Do, YYYY") : d.format("MMMM Do, YYYY HH:mm");
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
}
|
package/setup/katex.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineKatexSetup } from '@slidev/types'
|
|
2
|
+
import { colors } from '../scripts/color'
|
|
3
|
+
|
|
4
|
+
// Short color macros, e.g. \red{x} instead of \htmlStyle{color:var(--theme-red)}{x}.
|
|
5
|
+
// \red{x} is default shade, \red{2}{x} or \red2{x} for the "2"nd shade.
|
|
6
|
+
const tokensToText = (tokens: { text: string }[]) => tokens.slice().reverse().map((t) => t.text).join('')
|
|
7
|
+
|
|
8
|
+
const makeColorMacro = (name: string, shadeKeys: Set<string>) => (context: any) => {
|
|
9
|
+
context.consumeSpaces()
|
|
10
|
+
const first = context.consumeArg()
|
|
11
|
+
const firstText = tokensToText(first.tokens)
|
|
12
|
+
const hasShadeArg = shadeKeys.has(firstText) && context.future().text === '{'
|
|
13
|
+
const varName = hasShadeArg ? `${name}-${firstText}` : name
|
|
14
|
+
const textArg = hasShadeArg ? tokensToText(context.consumeArg().tokens) : firstText
|
|
15
|
+
return `\\htmlStyle{color:var(--theme-${varName})}{${textArg}}`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const colorMacros = Object.fromEntries(
|
|
19
|
+
Object.entries(colors).map(([name, value]) => {
|
|
20
|
+
const shadeKeys = typeof value === 'string'
|
|
21
|
+
? new Set<string>()
|
|
22
|
+
: new Set(Object.keys(value).filter((k) => k !== 'DEFAULT'))
|
|
23
|
+
return [`\\${name}`, makeColorMacro(name, shadeKeys)]
|
|
24
|
+
}),
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
export default defineKatexSetup(() => {
|
|
28
|
+
return {
|
|
29
|
+
// maxExpand: 2000,
|
|
30
|
+
trust: true,
|
|
31
|
+
strict: false,
|
|
32
|
+
// throwOnError: false,
|
|
33
|
+
macros: colorMacros,
|
|
34
|
+
}
|
|
35
|
+
})
|
package/setup/shiki.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ShikiSetupReturn } from '@slidev/types'
|
|
2
|
+
import { defineShikiSetup } from '@slidev/types'
|
|
3
|
+
import { bundledThemes } from 'shiki'
|
|
4
|
+
import { colors } from '../scripts/color'
|
|
5
|
+
|
|
6
|
+
// Recolor a handful of syntax categories with the TUD corporate palette on
|
|
7
|
+
// top of a well-tested base theme, so code blocks pick up brand accents
|
|
8
|
+
// without losing that theme's contrast/hierarchy for everything else
|
|
9
|
+
// (comments, punctuation, generic variables, ...).
|
|
10
|
+
// Never touch comments — some grammars file them under oddly-named scopes
|
|
11
|
+
// (e.g. vitesse's `string.comment`) that would otherwise false-match below.
|
|
12
|
+
const isComment = (s: string) => s.split('.').includes('comment')
|
|
13
|
+
|
|
14
|
+
const CATEGORIES: { match: (scope: string) => boolean, color: (shade: 1 | 2) => string }[] = [
|
|
15
|
+
{ match: s => s === 'keyword' || s.startsWith('keyword.control') || s.startsWith('storage.type.class') || s.startsWith('storage.modifier'), color: shade => colors.blue[shade] },
|
|
16
|
+
{ match: s => s.split('.')[0] === 'string' || s.startsWith('source.regexp'), color: shade => colors.green[shade] },
|
|
17
|
+
{ match: s => s.includes('entity.name.function') || s.includes('support.function'), color: shade => colors.magenta[shade] },
|
|
18
|
+
{ match: s => s.includes('entity.name.tag') || s === 'tag.html', color: shade => colors.red[shade] },
|
|
19
|
+
{ match: s => s.includes('entity.name.type') || s.includes('entity.name.class') || s.includes('support.type') || s.includes('support.class') || s === 'namespace', color: shade => colors.violet[shade] },
|
|
20
|
+
{ match: s => s.includes('constant.numeric') || s === 'number', color: shade => colors.orange[shade] },
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
// shade 1 (saturated) reads well on light backgrounds, shade 2 (pastel) on dark ones.
|
|
24
|
+
function recolor(theme: any, shade: 1 | 2) {
|
|
25
|
+
return {
|
|
26
|
+
...theme,
|
|
27
|
+
tokenColors: theme.tokenColors?.map((rule: any) => {
|
|
28
|
+
const scopes: string[] = (Array.isArray(rule.scope) ? rule.scope : [rule.scope]).filter(Boolean)
|
|
29
|
+
if (scopes.some(isComment)) return rule
|
|
30
|
+
const category = CATEGORIES.find(c => scopes.some(c.match))
|
|
31
|
+
if (!category || !rule.settings?.foreground) return rule
|
|
32
|
+
return { ...rule, settings: { ...rule.settings, foreground: category.color(shade) } }
|
|
33
|
+
}),
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default defineShikiSetup(async (): Promise<ShikiSetupReturn> => {
|
|
38
|
+
const [{ default: light }, { default: dark }] = await Promise.all([
|
|
39
|
+
bundledThemes['night-owl-light'](),
|
|
40
|
+
bundledThemes['vitesse-dark'](),
|
|
41
|
+
])
|
|
42
|
+
return {
|
|
43
|
+
themes: {
|
|
44
|
+
light: recolor(light, 1),
|
|
45
|
+
dark: recolor(dark, 2),
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
})
|
package/shims.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
declare module '@slidev/client'
|
|
2
|
+
|
|
3
|
+
declare module '*.vue' {
|
|
4
|
+
import type { DefineComponent } from 'vue'
|
|
5
|
+
const component: DefineComponent<{}, {}, any>
|
|
6
|
+
export default component
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
declare module '*.svg' {
|
|
10
|
+
const src: string
|
|
11
|
+
export default src
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
declare module '*.svg?raw' {
|
|
15
|
+
const src: string
|
|
16
|
+
export default src
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module '*.ico?url' {
|
|
20
|
+
const src: string
|
|
21
|
+
export default src
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare module '*.png?url' {
|
|
25
|
+
const src: string
|
|
26
|
+
export default src
|
|
27
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Custom transitions with a smooth slide and fade animation
|
|
3
|
+
* Far more subtle than the build-in
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ```
|
|
7
|
+
* transition: slide-fade-left | slide-fade-right
|
|
8
|
+
* ```
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
.slide-fade-right-enter-active,
|
|
12
|
+
.slide-fade-left-enter-active {
|
|
13
|
+
transition:
|
|
14
|
+
opacity var(--slidev-transition-duration) ease
|
|
15
|
+
calc(var(--slidev-transition-duration) * 0.5), /* some additional delay here */
|
|
16
|
+
transform calc(var(--slidev-transition-duration) * 1.5) ease;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.slide-fade-right-leave-active,
|
|
20
|
+
.slide-fade-left-leave-active {
|
|
21
|
+
transition:
|
|
22
|
+
opacity var(--slidev-transition-duration) ease,
|
|
23
|
+
transform calc(var(--slidev-transition-duration) * 1.5) ease;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.slide-fade-right-leave-to,
|
|
27
|
+
.slide-fade-left-enter-from {
|
|
28
|
+
opacity: 0;
|
|
29
|
+
transform: translateX(4%);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.slide-fade-right-enter-from,
|
|
33
|
+
.slide-fade-left-leave-to {
|
|
34
|
+
opacity: 0;
|
|
35
|
+
transform: translateX(-4%);
|
|
36
|
+
}
|
package/styles/font.css
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: NotoSans;
|
|
3
|
+
src: url('../assets/fonts/NotoSans-VariableFont_wdth\,wght.ttf') format('truetype');
|
|
4
|
+
font-style: normal;
|
|
5
|
+
font-weight: 100 900;
|
|
6
|
+
font-stretch: 100%;
|
|
7
|
+
font-display: swap;
|
|
8
|
+
font-optical-sizing: auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
@font-face {
|
|
12
|
+
font-family: NotoSans;
|
|
13
|
+
src: url('../assets/fonts/NotoSans-Italic-VariableFont_wdth\,wght.ttf') format('truetype');
|
|
14
|
+
font-style: italic;
|
|
15
|
+
font-weight: 100 900;
|
|
16
|
+
font-stretch: 100%;
|
|
17
|
+
font-display: swap;
|
|
18
|
+
font-optical-sizing: auto;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@font-face {
|
|
22
|
+
font-family: FiraCode;
|
|
23
|
+
src: url('../assets/fonts/FiraCode-VariableFont_wght.ttf') format('truetype');
|
|
24
|
+
font-style: normal;
|
|
25
|
+
font-weight: 300 700;
|
|
26
|
+
font-display: swap;
|
|
27
|
+
font-optical-sizing: auto;
|
|
28
|
+
}
|