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 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: "Verified System Design Automation"
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': !show_footer}" class="duration-[0.5s] transition-opacity text-primary">
24
- <div v-if="$slidev.configs.footer !== false" class="whitespace-pre">
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': !show_footer}" class="duration-[0.5s] transition-opacity w-[66px]">
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
- const show_footer = computed(() => !new Set(['cover', 'cover-blue', 'cover-white', 'section', 'section-blue', 'section-white', 'section-n']).has($nav.value.currentLayout))
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
- const footer = computed(() => {
70
- let fstring = "{title} • {author}"
71
- return formatString(($slidev.configs as any).footer ?
72
- ($slidev.configs as any).footer : fstring,
73
- {...$slidev.configs, date: get_date.value}
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>
@@ -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">{{ $slidev.configs.author }}</p>
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>
@@ -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">{{ $slidev.configs.author }}</p>
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>
@@ -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
 
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidev-theme-tud",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "type": "module",
5
5
  "description": "Corporate TUD theme for SliDev",
6
6
  "keywords": [
@@ -1,22 +1,9 @@
1
1
  /// <reference types="vite/client" />
2
- import { reactive, toValue, watchEffect } from 'vue'
2
+ import { toValue } from 'vue'
3
3
  import type { MaybeRefOrGetter } from 'vue'
4
- import { useSlideContext } from '@slidev/client'
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
  }