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.
Files changed (56) hide show
  1. package/.envrc +1 -0
  2. package/.github/workflows/deploy.yml +84 -0
  3. package/.github/workflows/release.yml +89 -0
  4. package/.vscode/extensions.json +3 -0
  5. package/README.md +60 -0
  6. package/assets/TUD-logo-bl.svg +1 -0
  7. package/assets/TUD-logo-no-color.svg +1 -0
  8. package/assets/TUD-logo-text-no-color.svg +1 -0
  9. package/assets/TUD-logo-text-small-no-color.svg +1 -0
  10. package/assets/TUD-logo-tr.svg +1 -0
  11. package/assets/favicons/apple-touch-icon.png +0 -0
  12. package/assets/favicons/dark/apple-touch-icon.png +0 -0
  13. package/assets/favicons/dark/favicon-16x16.png +0 -0
  14. package/assets/favicons/dark/favicon-32x32.png +0 -0
  15. package/assets/favicons/dark/favicon.ico +0 -0
  16. package/assets/favicons/favicon-16x16.png +0 -0
  17. package/assets/favicons/favicon-32x32.png +0 -0
  18. package/assets/favicons/favicon.ico +0 -0
  19. package/assets/fonts/FiraCode-VariableFont_wght.ttf +0 -0
  20. package/assets/fonts/NotoSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  21. package/assets/fonts/NotoSans-VariableFont_wdth,wght.ttf +0 -0
  22. package/assets/fonts/OFL.txt +93 -0
  23. package/components/.gitkeep +0 -0
  24. package/components/Background.vue +130 -0
  25. package/components/Footnotes.vue +164 -0
  26. package/example.mdc +372 -0
  27. package/flake.lock +61 -0
  28. package/flake.nix +22 -0
  29. package/global-bottom.vue +79 -0
  30. package/global-top.vue +72 -0
  31. package/layouts/README.md +2 -0
  32. package/layouts/cols.vue +38 -0
  33. package/layouts/cover-blue.vue +60 -0
  34. package/layouts/cover-white.vue +58 -0
  35. package/layouts/cover.vue +10 -0
  36. package/layouts/default.vue +15 -0
  37. package/layouts/section-blue.vue +32 -0
  38. package/layouts/section-n.vue +171 -0
  39. package/layouts/section-white.vue +25 -0
  40. package/layouts/section.vue +10 -0
  41. package/markdown.ts +196 -0
  42. package/package.json +70 -0
  43. package/pnpm-workspace.yaml +23 -0
  44. package/scripts/background.ts +69 -0
  45. package/scripts/color.ts +81 -0
  46. package/scripts/util.ts +51 -0
  47. package/setup/katex.ts +35 -0
  48. package/setup/shiki.ts +48 -0
  49. package/shims.d.ts +27 -0
  50. package/styles/fade-transition.css +36 -0
  51. package/styles/font.css +28 -0
  52. package/styles/index.ts +5 -0
  53. package/styles/layout.css +184 -0
  54. package/tsconfig.json +30 -0
  55. package/uno.config.ts +35 -0
  56. 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 (`![alt](img){.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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ // inherit from base layouts, remove it to get full customizations
2
+ import '@slidev/client/styles/layouts-base.css'
3
+ import './layout.css'
4
+ import './fade-transition.css'
5
+ import './font.css'