slidev-theme-tud 0.0.8 → 0.0.10
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/README.md +6 -0
- package/example.mdc +7 -2
- package/global-bottom.vue +19 -13
- package/layouts/cover-blue.vue +7 -1
- package/layouts/cover-white.vue +7 -1
- package/layouts/section-blue.vue +2 -0
- package/layouts/section-n.vue +2 -0
- package/layouts/section-white.vue +2 -0
- package/package.json +1 -1
- package/scripts/background.ts +2 -15
- package/scripts/footer.ts +48 -0
- package/scripts/util.ts +38 -2
package/README.md
CHANGED
|
@@ -35,6 +35,12 @@ This theme provides the following components:
|
|
|
35
35
|
|
|
36
36
|
None
|
|
37
37
|
|
|
38
|
+
## Composables
|
|
39
|
+
|
|
40
|
+
The background, logos and footer of a slide can be changed from a layout or from
|
|
41
|
+
a single slide. See [docs/composables.md](./docs/composables.md) for what each
|
|
42
|
+
composable accepts.
|
|
43
|
+
|
|
38
44
|
## Contributing
|
|
39
45
|
|
|
40
46
|
- `pnpm install`
|
package/example.mdc
CHANGED
|
@@ -3,10 +3,10 @@ theme: ./
|
|
|
3
3
|
# Hash-based routing to make reload work on GitHub Pages
|
|
4
4
|
routerMode: hash
|
|
5
5
|
# layout: "cover"
|
|
6
|
-
author: "Your Name"
|
|
6
|
+
author: "Someone else, <u>Your Name</u>, Your Prof."
|
|
7
7
|
title: "Some very catchy title"
|
|
8
8
|
subtitle: "With a sub-title"
|
|
9
|
-
group: "
|
|
9
|
+
group: "Here goes your group"
|
|
10
10
|
location: "Dresden"
|
|
11
11
|
|
|
12
12
|
# Use single quotes so YAML keeps the backslash literally (double quotes turn \t etc. into escapes).
|
|
@@ -224,6 +224,11 @@ normal lists:
|
|
|
224
224
|
|
|
225
225
|
---
|
|
226
226
|
|
|
227
|
+
<script setup>
|
|
228
|
+
import { useFooter } from "./scripts/footer"
|
|
229
|
+
const footer = useFooter({ template: null })
|
|
230
|
+
</script>
|
|
231
|
+
|
|
227
232
|
# Latex support
|
|
228
233
|
|
|
229
234
|
Did you ever use google slides for a theoretical presentation?
|
package/global-bottom.vue
CHANGED
|
@@ -20,10 +20,8 @@
|
|
|
20
20
|
></div>
|
|
21
21
|
|
|
22
22
|
<footer class="absolute bottom-0 h-16 pb-[13px] pr-[64px] pl-[164px] flex items-baseline gap-x-6 w-full z-100">
|
|
23
|
-
<div :class="{'opacity-0': !
|
|
24
|
-
<div v-
|
|
25
|
-
{{ footer }}
|
|
26
|
-
</div>
|
|
23
|
+
<div :class="{'opacity-0': !footer}" class="duration-[0.5s] transition-opacity text-primary">
|
|
24
|
+
<div v-html="footer" class="whitespace-pre"></div>
|
|
27
25
|
</div>
|
|
28
26
|
|
|
29
27
|
<!-- Footnotes teleport target (see components/Footnotes.vue) -->
|
|
@@ -31,7 +29,7 @@
|
|
|
31
29
|
<div :id="footnotesTargetId" class="absolute bottom-0 w-full min-h-full pb-2"></div>
|
|
32
30
|
</div>
|
|
33
31
|
|
|
34
|
-
<div :class="{'opacity-0': !
|
|
32
|
+
<div :class="{'opacity-0': !show_number}" class="duration-[0.5s] transition-opacity w-[66px]">
|
|
35
33
|
<div class="flex items-baseline justify-center">
|
|
36
34
|
<span class="flex-1 text-right font-bold text-lg text-primary">{{ $nav.currentPage }}</span>
|
|
37
35
|
<span :class="{ 'opacity-0': $nav.clicksTotal == 0 }" class="text-left text-gray text-xs">.</span>
|
|
@@ -47,6 +45,7 @@ import { computed, unref } from 'vue';
|
|
|
47
45
|
import { expandDateTokens } from './scripts/util';
|
|
48
46
|
import { formatString } from './scripts/util';
|
|
49
47
|
import { slideBackgrounds, slideBgLogos, slideLogos } from './scripts/background';
|
|
48
|
+
import { slideFooters, DEFAULT_FOOTER_TEMPLATE } from './scripts/footer';
|
|
50
49
|
|
|
51
50
|
import rawLogo from './assets/TUD-logo-no-color.svg?raw';
|
|
52
51
|
import rawLogoSmall from './assets/TUD-logo-text-small-no-color.svg?raw';
|
|
@@ -60,20 +59,27 @@ const footnotesTargetId = computed(() => `footnotes-containter-${unref(($nav as
|
|
|
60
59
|
|
|
61
60
|
const logos = computed(() => slideLogos[$nav.value.currentPage] ?? {})
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
// Per-slide settings published via `useFooter()` (see scripts/footer.ts);
|
|
63
|
+
// the layouts without room for a footer opt out there.
|
|
64
|
+
const slideFooter = computed(() => slideFooters[$nav.value.currentPage] ?? {})
|
|
65
|
+
|
|
66
|
+
const show_number = computed(() => slideFooter.value.number ?? true)
|
|
64
67
|
|
|
65
68
|
const get_date = computed(() => {
|
|
66
69
|
const d = ($slidev.configs as any).date
|
|
67
70
|
return d ? expandDateTokens(String(d)) : d
|
|
68
71
|
})
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
72
|
+
|
|
73
|
+
// slide (`useFooter`) > headmatter (`footer`, `false` for no text) > theme default
|
|
74
|
+
const footer_template = computed(() => {
|
|
75
|
+
if (slideFooter.value.template !== undefined) return slideFooter.value.template
|
|
76
|
+
const configured = ($slidev.configs as any).footer
|
|
77
|
+
if (configured === false) return null
|
|
78
|
+
return configured ? String(configured) : DEFAULT_FOOTER_TEMPLATE
|
|
75
79
|
})
|
|
80
|
+
const footer = computed(() => footer_template.value === null ? ''
|
|
81
|
+
: formatString(footer_template.value, {...$slidev.configs, date: get_date.value}))
|
|
76
82
|
|
|
77
83
|
const pageLogo = computed(() => slideBgLogos[$nav.value.currentPage])
|
|
78
84
|
|
|
79
|
-
</script>
|
|
85
|
+
</script>
|
package/layouts/cover-blue.vue
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
<div class="name absolute bottom-[120px]">
|
|
18
18
|
<div v-if="!slotTags.includes('p')">
|
|
19
|
-
<p class="!text-2xl !m-0 font-bold"
|
|
19
|
+
<p class="!text-2xl !m-0 font-bold" :innerHTML="$slidev.configs.author"></p>
|
|
20
20
|
<p class="!m-0 !text-sm">{{ $slidev.configs.group }}</p>
|
|
21
21
|
</div>
|
|
22
22
|
<component v-else v-for="node in get_slot('p')" class="!m-0 !text-sm" :is="node" :key="node.key"/>
|
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
import { useSlideContext } from '@slidev/client'
|
|
30
30
|
import { expandDateTokens } from '../scripts/util';
|
|
31
31
|
import { useBackground, useLogo } from '../scripts/background'
|
|
32
|
+
import { useFooter } from '../scripts/footer'
|
|
32
33
|
import { computed, useSlots } from 'vue'
|
|
33
34
|
|
|
34
35
|
useBackground('primary')
|
|
35
36
|
useLogo({ text: 'white' })
|
|
37
|
+
useFooter({ template: null, number: false })
|
|
36
38
|
|
|
37
39
|
const slotTags = computed(() => (useSlots().default?.() || []).map(e => e.type));
|
|
38
40
|
const { $slidev } = useSlideContext()
|
|
@@ -56,5 +58,9 @@ const get_date = computed(() => {
|
|
|
56
58
|
.title :deep(h2), .name :deep(p) {
|
|
57
59
|
color: white;
|
|
58
60
|
}
|
|
61
|
+
.name :deep(u) {
|
|
62
|
+
text-decoration-thickness: 3px;
|
|
63
|
+
text-underline-offset: 5px;
|
|
64
|
+
}
|
|
59
65
|
}
|
|
60
66
|
</style>
|
package/layouts/cover-white.vue
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
<div class="name absolute bottom-[120px] text-gray">
|
|
18
18
|
<div v-if="!slotTags.includes('p')">
|
|
19
|
-
<p class="!text-2xl !m-0 font-bold"
|
|
19
|
+
<p class="!text-2xl !m-0 font-bold" :innerHTML="$slidev.configs.author"></p>
|
|
20
20
|
<p class="!m-0 !text-sm">{{ $slidev.configs.group }}</p>
|
|
21
21
|
</div>
|
|
22
22
|
<component v-else v-for="node in get_slot('p')" class="!m-0 !text-sm" :is="node" :key="node.key"/>
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
import { useSlideContext } from '@slidev/client'
|
|
30
30
|
import { expandDateTokens } from '../scripts/util';
|
|
31
31
|
import { useLogo } from '../scripts/background'
|
|
32
|
+
import { useFooter } from '../scripts/footer'
|
|
32
33
|
import { computed, useSlots } from 'vue'
|
|
33
34
|
|
|
34
35
|
useLogo({ text: 'primary' })
|
|
36
|
+
useFooter({ template: null, number: false })
|
|
35
37
|
|
|
36
38
|
const slotTags = computed(() => (useSlots().default?.() || []).map(e => e.type));
|
|
37
39
|
const { $slidev } = useSlideContext()
|
|
@@ -54,5 +56,9 @@ const get_date = computed(() => {
|
|
|
54
56
|
.title :deep(h2) {
|
|
55
57
|
@apply text-gray;
|
|
56
58
|
}
|
|
59
|
+
.name :deep(u) {
|
|
60
|
+
text-decoration-thickness: 3px;
|
|
61
|
+
text-underline-offset: 5px;
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
</style>
|
package/layouts/section-blue.vue
CHANGED
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
<script setup lang="ts">
|
|
12
12
|
import { useBackground, useLogo } from '../scripts/background'
|
|
13
|
+
import { useFooter } from '../scripts/footer'
|
|
13
14
|
useBackground('primary')
|
|
14
15
|
useLogo({ small: 'white' })
|
|
16
|
+
useFooter({ template: null, number: false })
|
|
15
17
|
// TODO generalize over bg color via parameter
|
|
16
18
|
</script>
|
|
17
19
|
|
package/layouts/section-n.vue
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
<script setup lang="ts">
|
|
22
22
|
import { computed } from 'vue'
|
|
23
23
|
import { useBackground, useBackgroundLogo, useLogo, type BackgroundLogo, type MarkTransform } from '../scripts/background'
|
|
24
|
+
import { useFooter } from '../scripts/footer'
|
|
24
25
|
import { asIndex, resolveColorName, toCssColor } from '../scripts/color'
|
|
25
26
|
|
|
26
27
|
const { variant = 1, colors = {} } = defineProps<{
|
|
@@ -42,6 +43,7 @@ const textColor = computed(() => resolveColorName(text, 'text', defaultIndex))
|
|
|
42
43
|
const textCss = computed(() => toCssColor(textColor.value))
|
|
43
44
|
|
|
44
45
|
useBackground(bgColor)
|
|
46
|
+
useFooter({ template: null, number: false })
|
|
45
47
|
|
|
46
48
|
// SVG is 100x100; slide is 1280x720, and the transform origin is (50,50).
|
|
47
49
|
// Values feed MarkTransform (see scripts/background.ts): { scale, x, y, rotate? }.
|
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
</template>
|
|
10
10
|
<script setup lang="ts">
|
|
11
11
|
import { useLogo } from '../scripts/background'
|
|
12
|
+
import { useFooter } from '../scripts/footer'
|
|
12
13
|
|
|
13
14
|
useLogo({ small: 'primary' })
|
|
15
|
+
useFooter({ template: null, number: false })
|
|
14
16
|
</script>
|
|
15
17
|
<style>
|
|
16
18
|
.slidev-layout.section-white {
|
package/package.json
CHANGED
package/scripts/background.ts
CHANGED
|
@@ -1,22 +1,9 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
|
-
import {
|
|
2
|
+
import { toValue } from 'vue'
|
|
3
3
|
import type { MaybeRefOrGetter } from 'vue'
|
|
4
|
-
import {
|
|
4
|
+
import { createRegistry } from './util'
|
|
5
5
|
import { toCssColor, toRawColor } from './color'
|
|
6
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
7
|
const backgroundsReg = createRegistry<string>()
|
|
21
8
|
export const slideBackgrounds = backgroundsReg.store
|
|
22
9
|
export function useBackground(color: MaybeRefOrGetter<string>) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { toValue } from 'vue'
|
|
2
|
+
import type { MaybeRefOrGetter } from 'vue'
|
|
3
|
+
import { createRegistry } from './util'
|
|
4
|
+
|
|
5
|
+
/** The footer text used when neither a slide nor the headmatter provides one. */
|
|
6
|
+
export const DEFAULT_FOOTER_TEMPLATE = '{title} • {author}'
|
|
7
|
+
|
|
8
|
+
export interface Footer {
|
|
9
|
+
/**
|
|
10
|
+
* The footer text, or `null` for none.
|
|
11
|
+
* Defaults to the headmatter's `footer`, else {@link DEFAULT_FOOTER_TEMPLATE}.
|
|
12
|
+
*/
|
|
13
|
+
template?: string | null
|
|
14
|
+
/** Whether the page number (+ click counter) is shown (default: `true`) */
|
|
15
|
+
number?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Shorthand accepted by {@link useFooter}: a `string` or `null` sets {@link Footer.template}. */
|
|
19
|
+
export type FooterInput = string | null | Footer
|
|
20
|
+
|
|
21
|
+
const footerReg = createRegistry<Footer>()
|
|
22
|
+
export const slideFooters = footerReg.store
|
|
23
|
+
|
|
24
|
+
/** Normalize the {@link useFooter} shorthands into a {@link Footer}. */
|
|
25
|
+
export function toFooter(footer: FooterInput | undefined): Footer {
|
|
26
|
+
if (footer === undefined) return {}
|
|
27
|
+
if (footer === null || typeof footer === 'string') return { template: footer }
|
|
28
|
+
return footer
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Override the footer of the slide this is called on.
|
|
33
|
+
*
|
|
34
|
+
* Call it from a layout's or a slide's `setup`. The text and the page number
|
|
35
|
+
* are independent, and each field left unset keeps its default — so the footer
|
|
36
|
+
* stays visible with its usual content unless asked otherwise.
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* useFooter({ template: null, number: false }) // no footer at all
|
|
40
|
+
* useFooter('{title} — {subtitle}') // keep it, change the text
|
|
41
|
+
* useFooter(null) // only the page number
|
|
42
|
+
* useFooter({ number: false }) // only the text
|
|
43
|
+
* useFooter(() => (done ? null : '{title}')) // reactive: refs and getters work too
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useFooter(footer: MaybeRefOrGetter<FooterInput>) {
|
|
47
|
+
footerReg.publish(() => toFooter(toValue(footer)))
|
|
48
|
+
}
|
package/scripts/util.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// "doesn't provide an export named: 'default'". The `/esm` build is proper ESM.
|
|
5
5
|
import dayjs from "dayjs/esm";
|
|
6
6
|
import advancedFormat from "dayjs/esm/plugin/advancedFormat";
|
|
7
|
-
import { inject, ref } from "vue";
|
|
8
|
-
import type { Ref } from "vue";
|
|
7
|
+
import { getCurrentInstance, inject, reactive, ref, toValue, watchEffect } from "vue";
|
|
8
|
+
import type { MaybeRefOrGetter, Ref } from "vue";
|
|
9
|
+
import { useSlideContext } from "@slidev/client";
|
|
9
10
|
|
|
10
11
|
export const WIDTH = 1280
|
|
11
12
|
export const HEIGHT = 720
|
|
@@ -26,6 +27,41 @@ export function useScale(): Ref<number> {
|
|
|
26
27
|
return inject<Ref<number>>("$$slidev-slide-scale", ref(1));
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// How deeply the calling component is nested. A slide's own `<script setup>`
|
|
31
|
+
// runs in the component that *renders* the layout, so it always sits above it.
|
|
32
|
+
function componentDepth() {
|
|
33
|
+
let depth = 0
|
|
34
|
+
for (let i = getCurrentInstance()?.parent; i; i = i.parent) depth++
|
|
35
|
+
return depth
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* A per-slide value store.
|
|
40
|
+
*
|
|
41
|
+
* `publish` is meant to be called from a layout's (or a slide's) `setup`; it
|
|
42
|
+
* records the value under the slide it was called on, so that the globals
|
|
43
|
+
* (e.g. `global-bottom.vue`) can look it up by page number. Values stay
|
|
44
|
+
* reactive: refs/getters are re-evaluated whenever their sources change.
|
|
45
|
+
*/
|
|
46
|
+
export function createRegistry<T>() {
|
|
47
|
+
const store = reactive<Record<number, T>>({})
|
|
48
|
+
// keep outside of `store` to avoid `watchEffect` dep. cycle
|
|
49
|
+
const owner: Record<number, number> = {}
|
|
50
|
+
|
|
51
|
+
function publish(value: MaybeRefOrGetter<T>) {
|
|
52
|
+
const page = toValue(useSlideContext().$page)
|
|
53
|
+
const depth = componentDepth() // fix priority order
|
|
54
|
+
watchEffect(() => {
|
|
55
|
+
const resolved = toValue(value)
|
|
56
|
+
if (owner[page] !== undefined && depth > owner[page]) return
|
|
57
|
+
owner[page] = depth
|
|
58
|
+
store[page] = resolved
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { store, publish }
|
|
63
|
+
}
|
|
64
|
+
|
|
29
65
|
export function formatString(template : string, values : any) {
|
|
30
66
|
return template.replace(/{(\w+)}/g, (_, key) => values[key] ?? `{${key}}`);
|
|
31
67
|
}
|