slidev-theme-tud-db 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.
@@ -0,0 +1,81 @@
1
+ <script setup lang="ts">
2
+ import { useSlideContext } from '@slidev/client'
3
+ import { computed } from 'vue'
4
+
5
+ import mosaic from '../assets/section-mosaic.svg'
6
+ import tudWhite from '../assets/tud-white.svg'
7
+ import ddrgWhite from '../assets/ddrg-white.svg'
8
+
9
+ // The large glyph in the blue area can be overridden per slide via the `num`
10
+ // frontmatter, e.g.
11
+ // layout: section-num
12
+ // num: IV
13
+ // By default it is the slide's position in the table of contents: its 1-based
14
+ // index among its siblings at its own TOC level. Slides missing from the TOC
15
+ // (no title, or `hideInToc`) show no glyph.
16
+ const props = defineProps({
17
+ num: { type: [String, Number], default: '' },
18
+ })
19
+
20
+ const { $nav, $page } = useSlideContext()
21
+
22
+ interface TocEntry { no: number, children: TocEntry[] }
23
+
24
+ // 1-based index of the slide numbered `no` among its siblings, or 0 if absent
25
+ function tocIndexOf(items: TocEntry[], no: number): number {
26
+ for (const [i, item] of items.entries()) {
27
+ if (item.no === no)
28
+ return i + 1
29
+ const nested = tocIndexOf(item.children, no)
30
+ if (nested)
31
+ return nested
32
+ }
33
+ return 0
34
+ }
35
+
36
+ const num = computed(() => {
37
+ if (String(props.num) !== '')
38
+ return props.num
39
+ return tocIndexOf($nav.value?.tocTree ?? [], $page.value) || ''
40
+ })
41
+ </script>
42
+
43
+ <template>
44
+ <div class="slidev-layout db-cover p-0">
45
+ <div class="relative w-full h-[345px] bg-[#2f57b2]">
46
+ <!-- glyph is painted using bg-clip to circumvent firefox' font scaling limit -->
47
+ <!-- See https://bugzilla.mozilla.org/show_bug.cgi?id=1677551 -->
48
+ <div
49
+ v-if="num !== ''"
50
+ class="absolute font-700 text-[24rem]/[24rem] pt-8 px-4 bg-white bg-clip-text text-transparent"
51
+ >{{ num }}</div>
52
+ <div class="absolute flex gap-[45px] top-[25px] right-[35px]">
53
+ <img
54
+ class="h-[45px]"
55
+ :src="tudWhite"
56
+ alt="TU Dresden"
57
+ />
58
+ <img
59
+ class="h-[45px]"
60
+ :src="ddrgWhite"
61
+ alt="Dresden Database Research Group"
62
+ />
63
+ </div>
64
+ </div>
65
+
66
+ <!-- white area: title (#) + subtitle (following text) -->
67
+ <div class="db-cover__title relative m-[30px]">
68
+ <slot />
69
+ </div>
70
+ </div>
71
+ </template>
72
+
73
+ <style scoped>
74
+ .db-cover__title :deep(h1) {
75
+ @apply text-4xl! font-700 text-[var(--db-heading-blue)];
76
+ }
77
+ .db-cover__title :deep(p) {
78
+ @apply absolute top-[95px] m-0 text-md;
79
+ color: var(--db-text-soft);
80
+ }
81
+ </style>
@@ -0,0 +1,37 @@
1
+ <script setup lang="ts">
2
+ // "Titelfolie" — navy band (top, with the DDRG mosaic + both logos) + white
3
+ // title area below. Reconstructed as pure vector: the navy is a CSS fill, the
4
+ // corner mosaic is `cover-mosaic.svg` (rebuilt from the original DB_template.potx
5
+ // title-slide tiles), and the logos are the theme's own white SVGs. No raster.
6
+ // Geometry (positions/sizes in pt == px on the 720-wide canvas) is taken from
7
+ // slideLayout1.xml ("Titelfolie") so it matches the source template.
8
+ import mosaic from '../assets/section-mosaic.svg'
9
+ import tudWhite from '../assets/tud-white.svg'
10
+ import ddrgWhite from '../assets/ddrg-white.svg'
11
+ </script>
12
+
13
+ <template>
14
+ <div class="slidev-layout db-cover p-0">
15
+ <!-- navy band: solid fill + vector mosaic + logos, all overlaid -->
16
+ <div class="relative w-full mt-[95px] h-[250px] bg-[#97c6ff] overflow-hidden">
17
+ <img class="absolute h-full scale-[1.01]" :src="mosaic" alt="" />
18
+ </div>
19
+
20
+ <!-- white area: title (#) + subtitle (following text) -->
21
+ <div class="db-cover__title relative m-[30px]">
22
+ <slot />
23
+ </div>
24
+ </div>
25
+ </template>
26
+
27
+ <style scoped>
28
+ .db-cover__title :deep(h1) {
29
+ @apply text-4xl! font-700 text-[var(--db-heading-blue)];
30
+ }
31
+ .db-cover__title :deep(p) {
32
+ @apply absolute top-[95px] m-0 text-md;
33
+ /* margin: 16px 0 0; */
34
+ /* font-size: 15px; */
35
+ color: var(--db-text-soft);
36
+ }
37
+ </style>
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ // Like two-cols-header, but the two columns are laid out as a ROW GRID: every
3
+ // top-level <div> in the `::grid::` slot becomes a cell, filling left, right,
4
+ // left, right… Because CSS grid rows align across columns, paired items (e.g. a
5
+ // heading on the left and one on the right) line up automatically and stay
6
+ // aligned when content changes.
7
+ //
8
+ // ---
9
+ // layout: two-cols-grid
10
+ // columns: 55fr 45fr # optional ratio (default 1fr 1fr)
11
+ // ---
12
+ // # Title
13
+ // ::grid::
14
+ // <div> … left of row 1 … </div>
15
+ // <div> … right of row 1 … </div>
16
+ // <div> … left of row 2 … </div>
17
+ // <div> … right of row 2 … </div>
18
+ const props = defineProps({
19
+ columns: { type: String, default: '1fr 1fr' },
20
+ codeSize: { type: String, default: '' },
21
+ })
22
+ </script>
23
+
24
+ <template>
25
+ <DbFrame :code-size="props.codeSize">
26
+ <div class="db-header"><slot /></div>
27
+
28
+ <div class="db-rowgrid" :style="{ gridTemplateColumns: props.columns }">
29
+ <slot name="grid" />
30
+ </div>
31
+ </DbFrame>
32
+ </template>
33
+
34
+ <style scoped>
35
+ .db-rowgrid {
36
+ display: grid;
37
+ column-gap: 28px;
38
+ row-gap: 12px;
39
+ align-items: start; /* each cell starts at the top of its row */
40
+ }
41
+ </style>
@@ -0,0 +1,28 @@
1
+ <script setup lang="ts">
2
+ // "Zwei Inhalte": full-width title (default slot) + two columns. Chrome via DbFrame.
3
+ // Column ratio is configurable per slide via the `columns` frontmatter, e.g.
4
+ // layout: two-cols-header
5
+ // columns: 3fr 2fr # 60/40 (default is 1fr 1fr = 50/50)
6
+ const props = defineProps({
7
+ columns: { type: String, default: '1fr 1fr' },
8
+ codeSize: { type: String, default: '' },
9
+ })
10
+ </script>
11
+
12
+ <template>
13
+ <DbFrame :code-size="props.codeSize">
14
+ <div class="db-header"><slot /></div>
15
+
16
+ <div class="db-grid" :style="{ gridTemplateColumns: props.columns }">
17
+ <div class="db-grid__col"><slot name="left" /></div>
18
+ <div class="db-grid__col"><slot name="right" /></div>
19
+ </div>
20
+ </DbFrame>
21
+ </template>
22
+
23
+ <style scoped>
24
+ .db-grid {
25
+ display: grid;
26
+ column-gap: 28px;
27
+ }
28
+ </style>
package/markdown.ts ADDED
@@ -0,0 +1,197 @@
1
+ import type { MarkdownExit } from 'markdown-exit'
2
+ import { readFileSync, statSync } from 'node:fs'
3
+ import { parseSync } from '@slidev/parser'
4
+
5
+ // Slidev runs markdown through `markdown-exit` (a markdown-it fork), so the
6
+ // `md` passed to markdownSetup is a MarkdownExit instance, not a markdown-it one.
7
+
8
+ // Custom markdown-it customizations for the theme, wired up from
9
+ // `slidev.markdown.markdownSetup` in vite.config.ts:
10
+ // - footnotes teleported into a <Footnotes> component
11
+ // - LaTeX-style figure captions (`![alt](img){.caption}`)
12
+ // - LaTeX-style table captions (a `{.caption}` paragraph after a table)
13
+ // - deck-wide "Figure N:" / "Table N:" numbering
14
+
15
+ // --- Global figure/table numbering -----------------------------------------
16
+ // Figures and tables are numbered across the whole deck, not per slide. CSS
17
+ // counters can't do this: Slidev hides off-screen slides with `display: none`,
18
+ // and `counter-increment` is ignored on display:none elements — so a slide
19
+ // never "sees" the figures on other slides. Instead we bake the numbers in at
20
+ // build time. `env.id` (".../deck.md__slidev_<N>.md") tells us which slide is
21
+ // rendering; we parse the full deck once (cached by mtime), count the numbered
22
+ // `{.caption .numbered}` figures/tables on every earlier slide, and use those
23
+ // running totals as the starting offset for this slide.
24
+
25
+ interface DeckCounts { mtimeMs: number, fig: number[], tab: number[] }
26
+ const deckCountsCache = new Map<string, DeckCounts>()
27
+
28
+ // Count `{.caption .numbered}` figures (image) and tables (standalone
29
+ // paragraph) in one slide's raw markdown. Code spans/fences are stripped first
30
+ // so a `{.caption .numbered}` shown as example code is never miscounted.
31
+ function countNumbered(content: string): { fig: number, tab: number } {
32
+ const stripped = content
33
+ .replace(/```[\s\S]*?```/g, '')
34
+ .replace(/`[^`]*`/g, '')
35
+ let fig = 0
36
+ let tab = 0
37
+ const attrBlock = /\{([^{}]*)\}/g
38
+ let match: RegExpExecArray | null
39
+ while ((match = attrBlock.exec(stripped))) {
40
+ const classes = (match[1].match(/\.[\w-]+/g) ?? []).map((c) => c.slice(1))
41
+ if (!classes.includes('numbered')) continue
42
+ if (!classes.includes('table-caption') && !classes.includes('table-footer') && !classes.includes('caption'))
43
+ continue
44
+ if (/!\[[^\]]*\]\([^)]*\)\s*$/.test(stripped.slice(0, match.index)))
45
+ fig++
46
+ else
47
+ tab++
48
+ }
49
+ return { fig, tab }
50
+ }
51
+
52
+ function deckCounts(entryPath: string): DeckCounts | null {
53
+ let stat
54
+ try {
55
+ stat = statSync(entryPath)
56
+ }
57
+ catch {
58
+ return null
59
+ }
60
+ const cached = deckCountsCache.get(entryPath)
61
+ if (cached && cached.mtimeMs === stat.mtimeMs)
62
+ return cached
63
+ const parsed = parseSync(readFileSync(entryPath, 'utf-8'), entryPath)
64
+ const counts: DeckCounts = { mtimeMs: stat.mtimeMs, fig: [], tab: [] }
65
+ for (const slide of parsed.slides) {
66
+ const { fig, tab } = countNumbered(slide.content ?? '')
67
+ counts.fig.push(fig)
68
+ counts.tab.push(tab)
69
+ }
70
+ deckCountsCache.set(entryPath, counts)
71
+ return counts
72
+ }
73
+
74
+ // How many numbered figures/tables appear on slides *before* the one rendering
75
+ // now — i.e. the offset this slide's numbering should start from.
76
+ function numberingOffset(env: any): { fig: number, tab: number } {
77
+ const id = /^(.*)__slidev_(\d+)\.md$/.exec(env?.id ?? '')
78
+ if (!id)
79
+ return { fig: 0, tab: 0 }
80
+ const counts = deckCounts(id[1])
81
+ if (!counts)
82
+ return { fig: 0, tab: 0 }
83
+ const slideIndex = Number(id[2]) - 1
84
+ let fig = 0
85
+ let tab = 0
86
+ for (let i = 0; i < slideIndex && i < counts.fig.length; i++) {
87
+ fig += counts.fig[i]
88
+ tab += counts.tab[i]
89
+ }
90
+ return { fig, tab }
91
+ }
92
+
93
+ // Gather every class on a token, coping with both markdown-it-attrs (one
94
+ // space-separated `class` attr) and Slidev's MDC (a separate `class` attr per
95
+ // class, e.g. [["class","caption"],["class","numbered"]]).
96
+ function classesOf(token: { attrs?: [string, string][] | null }): string[] {
97
+ return (token.attrs ?? [])
98
+ .filter(([name]) => name === 'class')
99
+ .flatMap(([, value]) => value.split(/\s+/))
100
+ .filter(Boolean)
101
+ }
102
+
103
+ // Footnotes: wrap the rendered footnotes block in a <Footnotes> component (see
104
+ // components/Footnotes.vue) instead of emitting it inline in the slide. The
105
+ // component teleports it into global-bottom, so footnotes live in the
106
+ // persistent layer: they don't animate with slide transitions and sit next to
107
+ // the footer. The inline footnote refs ([^1] markers) are produced by other
108
+ // rules and stay in the slide body.
109
+ export function setupFootnotes(md: MarkdownExit): void {
110
+ md.renderer.rules.footnote_block_open = () =>
111
+ '<Footnotes>\n<section class="footnotes">\n<ol class="footnotes-list">\n'
112
+ md.renderer.rules.footnote_block_close = () =>
113
+ '</ol>\n</section>\n</Footnotes>\n'
114
+ }
115
+
116
+ // LaTeX-style figure captions: an image marked with `{.caption}` gets its alt
117
+ // text rendered as a caption below it. Add `{.numbered}` too for a "Figure N:"
118
+ // prefix (numbered deck-wide — see numberingOffset). We wrap the <img> and the
119
+ // caption in inline <span>s (not a <figure>) so the markup stays valid inside
120
+ // the <p> that markdown puts a standalone image in. Styling lives in
121
+ // styles/layout.css.
122
+ export function setupFigureCaptions(md: MarkdownExit): void {
123
+ const defaultImage = md.renderer.rules.image
124
+ ?? ((tokens, idx, options, env, self) => self.renderToken(tokens, idx, options))
125
+ md.renderer.rules.image = (tokens, idx, options, env, self) => {
126
+ const token = tokens[idx]
127
+ const img = defaultImage(tokens, idx, options, env, self)
128
+ const cls = token.attrGet('class') ?? ''
129
+ if (!/(^|\s)caption(\s|$)/.test(cls))
130
+ return img
131
+ // `token.content` is the image's alt text.
132
+ const alt = md.utils.escapeHtml(token.content ?? '')
133
+ let label = ''
134
+ if (/(^|\s)numbered(\s|$)/.test(cls)) {
135
+ // Continue the figure count from earlier slides; images render in
136
+ // document order, so a per-render running tally stays correct.
137
+ const e = env as { __figureNo?: number }
138
+ e.__figureNo ??= numberingOffset(env).fig
139
+ label = `<span class="caption-label">Figure ${++e.__figureNo}: </span>`
140
+ }
141
+ return `<span class="caption-figure">${img}<span class="caption-text">${label}${alt}</span></span>`
142
+ }
143
+ }
144
+
145
+ // LaTeX-style table captions, the table analogue of the figure captions above.
146
+ // A paragraph marked `{.caption}` placed directly after a table becomes that
147
+ // table's caption; add `{.numbered}` for a "Table N:" prefix. We wrap the
148
+ // <table> and the caption in a `display: table` container (.caption-table) —
149
+ // the exact trick the figure captions use — so the caption wraps to the
150
+ // table's width instead of stretching it. Styling lives in styles/layout.css
151
+ // and reuses `.caption-text`.
152
+ export function setupTableCaptions(md: MarkdownExit): void {
153
+ md.core.ruler.push('table_caption', (state) => {
154
+ const tokens = state.tokens
155
+ const Token = state.Token
156
+
157
+ // First pass: find every paragraph that has .table-caption or .table-footer
158
+ const captions: { pInlineIdx: number, numbered: boolean, num?: number }[] = []
159
+ for (let i = 2; i < tokens.length; i++) {
160
+ if (tokens[i].type !== 'paragraph_close') continue
161
+ const pInline = tokens[i - 1]
162
+ const pOpen = tokens[i - 2]
163
+ if (pInline.type !== 'inline' || pOpen.type !== 'paragraph_open') continue
164
+
165
+ const classes = [pOpen, ...(pInline.children ?? [])].flatMap(classesOf)
166
+ if (classes.includes('table-caption') || classes.includes('table-footer')) {
167
+ captions.push({ pInlineIdx: i - 1, numbered: classes.includes('numbered') })
168
+ }
169
+ }
170
+
171
+ if (captions.length === 0) return
172
+
173
+ let tableNo = numberingOffset(state.env).tab
174
+ for (const caption of captions) {
175
+ if (caption.numbered) caption.num = ++tableNo
176
+ }
177
+
178
+ // Second pass: inject Tab N:
179
+ for (let k = captions.length - 1; k >= 0; k--) {
180
+ const { pInlineIdx, numbered, num } = captions[k]
181
+ const pInline = tokens[pInlineIdx]
182
+
183
+ // Remove the .numbered marker so it doesn't bleed
184
+ for (const token of [tokens[pInlineIdx - 1], ...(pInline.children ?? [])]) {
185
+ if (token.attrs) {
186
+ token.attrs = token.attrs.filter(([name, value]) => !(name === 'class' && value === 'numbered'))
187
+ }
188
+ }
189
+
190
+ if (numbered) {
191
+ const label = new Token('html_inline', '', 0)
192
+ label.content = `<span class="caption-label">Tab. ${num}: </span>`
193
+ ;(pInline.children ??= []).unshift(label)
194
+ }
195
+ }
196
+ })
197
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "slidev-theme-tud-db",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "description": "Slidev theme cloning the TU Dresden / Dresden Database Research Group PowerPoint template",
6
+ "license": "GPL-3.0-or-later",
7
+ "keywords": [
8
+ "slidev-theme",
9
+ "slidev"
10
+ ],
11
+ "homepage": "https://github.com/vmelisch/slidev-theme-db",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/vmelisch/slidev-theme-db.git"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/vmelisch/slidev-theme-db/issues"
18
+ },
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "assets",
24
+ "components",
25
+ "global-bottom.vue",
26
+ "layouts",
27
+ "markdown.ts",
28
+ "setup",
29
+ "styles",
30
+ "vite.config.ts",
31
+ "README.md"
32
+ ],
33
+ "engines": {
34
+ "slidev": ">=0.48.0",
35
+ "node": ">=18.0.0"
36
+ },
37
+ "dependencies": {
38
+ "shiki": "^4.2.0",
39
+ "slidev-addon-gsap": "^0.0.4"
40
+ },
41
+ "devDependencies": {
42
+ "@slidev/cli": "^52.16.0",
43
+ "@slidev/parser": "^52.16.0",
44
+ "vue": "^3.5.38"
45
+ },
46
+ "slidev": {
47
+ "defaults": {
48
+ "addons": [
49
+ "gsap"
50
+ ],
51
+ "aspectRatio": "16/9",
52
+ "canvasWidth": 960,
53
+ "colorSchema": "light",
54
+ "mdc": true,
55
+ "comark": true,
56
+ "presenter": true,
57
+ "titleTemplate": "%s",
58
+ "transition": "slide-fade-left | slide-fade-right",
59
+ "fonts": {
60
+ "sans": "NotoSans",
61
+ "mono": "FiraCode",
62
+ "provider": null
63
+ }
64
+ }
65
+ },
66
+ "scripts": {
67
+ "build": "slidev build examples/slides.md",
68
+ "dev": "slidev examples/slides.md --remote",
69
+ "export": "slidev export examples/slides.md"
70
+ }
71
+ }
package/setup/shiki.ts ADDED
@@ -0,0 +1,13 @@
1
+ // Default Shiki color scheme for fenced code blocks (```lang ... ```).
2
+ // Matches the inline-code highlighter (see vite.config.ts) and the chair's
3
+ // template, so inline and fenced code look the same.
4
+ //
5
+ // NB: no `import { defineShikiSetup } from '@slidev/types'` on purpose — it's
6
+ // only a type helper, and Slidev calls this default export as `setup(ctx)`.
7
+ // catppuccin-latte / -mocha are bundled with Shiki, so no extra deps needed.
8
+ export default () => ({
9
+ themes: {
10
+ light: 'catppuccin-latte',
11
+ dark: 'catppuccin-mocha',
12
+ },
13
+ })