@slidev/client 52.14.1 → 52.15.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 (44) hide show
  1. package/builtin/BlueSky.vue +167 -0
  2. package/builtin/CodeBlockWrapper.vue +1 -1
  3. package/builtin/Link.vue +2 -2
  4. package/builtin/Monaco.vue +1 -2
  5. package/builtin/Tweet.vue +1 -1
  6. package/builtin/VClick.ts +8 -0
  7. package/builtin/VClicks.ts +24 -4
  8. package/builtin/VDrag.vue +1 -0
  9. package/composables/useDragElements.ts +5 -2
  10. package/composables/useDrawings.ts +6 -6
  11. package/composables/useMousePosInSlide.ts +21 -0
  12. package/composables/usePreloadImages.ts +2 -1
  13. package/composables/useSlideInfo.ts +1 -2
  14. package/constants.ts +2 -1
  15. package/internals/DrawingControls.vue +2 -2
  16. package/internals/Goto.vue +1 -1
  17. package/internals/IconButton.vue +3 -1
  18. package/internals/LaserPointer.vue +66 -0
  19. package/internals/MenuButton.vue +1 -1
  20. package/internals/NavControls.vue +28 -18
  21. package/internals/PresenterMouse.vue +1 -1
  22. package/internals/Settings.vue +10 -1
  23. package/internals/ShikiEditor.vue +2 -4
  24. package/internals/SideEditor.vue +3 -1
  25. package/internals/SlideContainer.vue +18 -1
  26. package/logic/recording.ts +16 -8
  27. package/logic/slides.ts +23 -11
  28. package/modules/v-click.ts +42 -8
  29. package/package.json +23 -22
  30. package/pages/404.vue +3 -1
  31. package/pages/export.vue +5 -1
  32. package/pages/notes-edit.vue +3 -1
  33. package/pages/play.vue +2 -0
  34. package/pages/presenter.vue +18 -23
  35. package/setup/main.ts +1 -2
  36. package/setup/monaco.ts +4 -4
  37. package/setup/root.ts +1 -1
  38. package/shim-vue.d.ts +3 -1
  39. package/state/shared.ts +1 -0
  40. package/state/storage.ts +7 -0
  41. package/styles/index.css +8 -22
  42. package/styles/index.ts +23 -0
  43. package/styles/layouts-base.css +1 -1
  44. package/styles/vclick.css +54 -0
@@ -4,7 +4,7 @@ import { sharedState } from '../state/shared'
4
4
 
5
5
  <template>
6
6
  <div
7
- v-if="sharedState.cursor"
7
+ v-if="sharedState.cursor?.style === 'cursor'"
8
8
  class="absolute top-0 left-0 right-0 bottom-0 pointer-events-none text-xl"
9
9
  >
10
10
  <ph-cursor-fill
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { useWakeLock } from '@vueuse/core'
3
3
  import { useNav } from '../composables/useNav'
4
- import { hideCursorIdle, slideScale, viewerCssFilter, viewerCssFilterDefaults, wakeLockEnabled } from '../state'
4
+ import { cursorStyle, hideCursorIdle, slideScale, viewerCssFilter, viewerCssFilterDefaults, wakeLockEnabled } from '../state'
5
5
  import FormCheckbox from './FormCheckbox.vue'
6
6
  import FormItem from './FormItem.vue'
7
7
  import FormSlider from './FormSlider.vue'
@@ -86,6 +86,15 @@ const { isSupported } = useWakeLock()
86
86
  />
87
87
  </FormItem>
88
88
  <div class="h-1px opacity-5 bg-current w-full my2" />
89
+ <FormItem title="Cursor Style">
90
+ <SegmentControl
91
+ v-model="cursorStyle"
92
+ :options="[
93
+ { label: 'Cursor', value: 'cursor' },
94
+ { label: 'Laser', value: 'laser' },
95
+ ]"
96
+ />
97
+ </FormItem>
89
98
  <FormItem
90
99
  v-if="!isPresenter"
91
100
  title="Slide Scale"
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { ref, shallowRef } from 'vue'
2
+ import { shallowRef } from 'vue'
3
3
  import { useIME } from '../composables/useIME'
4
4
 
5
5
  const props = defineProps<{
@@ -8,8 +8,6 @@ const props = defineProps<{
8
8
  const content = defineModel<string>({ required: true })
9
9
  const { composingContent, onInput, onCompositionEnd } = useIME(content)
10
10
 
11
- const textareaEl = ref<HTMLTextAreaElement | null>(null)
12
-
13
11
  const highlight = shallowRef<((code: string) => string) | null>(null)
14
12
  import('../setup/shiki').then(async (m) => {
15
13
  const { getEagerHighlighter, defaultHighlightOptions } = await m.default()
@@ -26,7 +24,7 @@ import('../setup/shiki').then(async (m) => {
26
24
  <div v-if="highlight" class="relative w-full h-max min-h-full">
27
25
  <div class="relative w-full h-max" v-html="`${highlight(composingContent)}&nbsp;`" />
28
26
  <textarea
29
- ref="textareaEl" v-model="composingContent" :placeholder="props.placeholder"
27
+ v-model="composingContent" :placeholder="props.placeholder"
30
28
  class="absolute inset-0 resize-none text-transparent bg-transparent focus:outline-none caret-black dark:caret-white overflow-y-hidden"
31
29
  @input="onInput"
32
30
  @compositionend="onCompositionEnd"
@@ -11,6 +11,8 @@ const props = defineProps<{
11
11
  resize?: boolean
12
12
  }>()
13
13
 
14
+ const RE_FRONTMATTER_BLOCK = /^---\n([\s\S]*?)\n---\n/
15
+
14
16
  const { currentSlideNo, openInEditor } = useNav()
15
17
 
16
18
  const tab = ref<'content' | 'note'>('content')
@@ -37,7 +39,7 @@ async function save() {
37
39
  dirty.value = false
38
40
 
39
41
  let frontmatterRaw: string | undefined
40
- const contentOnly = content.value.trim().replace(/^---\n([\s\S]*?)\n---\n/, (_, f) => {
42
+ const contentOnly = content.value.trim().replace(RE_FRONTMATTER_BLOCK, (_, f) => {
41
43
  frontmatterRaw = f
42
44
  return ''
43
45
  })
@@ -1,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { provideLocal, useElementSize } from '@vueuse/core'
3
- import { computed, onUnmounted, ref, watchEffect } from 'vue'
3
+ import { recomputeAllPoppers } from 'floating-vue'
4
+ import { computed, onMounted, onUnmounted, ref, watch, watchEffect, watchSyncEffect } from 'vue'
4
5
  import { useNav } from '../composables/useNav'
5
6
  import { injectionSlideElement, injectionSlideScale } from '../constants'
6
7
  import { slideAspect, slideHeight, slideWidth } from '../env'
@@ -72,6 +73,18 @@ if (props.isMain) {
72
73
  provideLocal(injectionSlideScale, scale)
73
74
  provideLocal(injectionSlideElement, slideElement)
74
75
 
76
+ watchSyncEffect(() => {
77
+ if (props.isMain) {
78
+ mainSlideElement.value = slideElement.value
79
+ }
80
+ })
81
+
82
+ onMounted(() => {
83
+ watch(() => props.isMain && scale.value, () => {
84
+ recomputeAllPoppers()
85
+ })
86
+ })
87
+
75
88
  const snapshot = computed(() => {
76
89
  if (props.no == null || !props.useSnapshot)
77
90
  return undefined
@@ -79,6 +92,10 @@ const snapshot = computed(() => {
79
92
  })
80
93
  </script>
81
94
 
95
+ <script lang="ts">
96
+ export const mainSlideElement = ref<HTMLElement | null>(null)
97
+ </script>
98
+
82
99
  <template>
83
100
  <div
84
101
  v-if="!snapshot"
@@ -3,6 +3,7 @@ import type { Options as RecorderOptions } from 'recordrtc'
3
3
  import type { Ref } from 'vue'
4
4
  import { isTruthy } from '@antfu/utils'
5
5
  import { useDevicesList, useEventListener, useLocalStorage } from '@vueuse/core'
6
+ import fixWebmDuration from 'fix-webm-duration'
6
7
  import { nextTick, ref, shallowRef, watch } from 'vue'
7
8
  import { currentCamera, currentMic } from '../state'
8
9
 
@@ -50,11 +51,11 @@ export const {
50
51
  } = useDevicesList({
51
52
  onUpdated() {
52
53
  if (currentCamera.value !== 'none') {
53
- if (!cameras.value.find(i => i.deviceId === currentCamera.value))
54
+ if (!cameras.value.some(i => i.deviceId === currentCamera.value))
54
55
  currentCamera.value = cameras.value[0]?.deviceId || 'default'
55
56
  }
56
57
  if (currentMic.value !== 'none') {
57
- if (!microphones.value.find(i => i.deviceId === currentMic.value))
58
+ if (!microphones.value.some(i => i.deviceId === currentMic.value))
58
59
  currentMic.value = microphones.value[0]?.deviceId || 'default'
59
60
  }
60
61
  },
@@ -78,6 +79,7 @@ export function useRecording() {
78
79
  const streamCamera: Ref<MediaStream | undefined> = shallowRef()
79
80
  const streamCapture: Ref<MediaStream | undefined> = shallowRef()
80
81
  const streamSlides: Ref<MediaStream | undefined> = shallowRef()
82
+ let recordingStartTime = 0
81
83
 
82
84
  const config: RecorderOptions = {
83
85
  type: 'video',
@@ -183,17 +185,18 @@ export function useRecording() {
183
185
  )
184
186
 
185
187
  recorderSlides.value.startRecording()
188
+ recordingStartTime = Date.now()
186
189
  recording.value = true
187
190
  }
188
191
 
189
192
  async function stopRecording() {
190
193
  recording.value = false
194
+ const duration = Date.now() - recordingStartTime
195
+
191
196
  recorderCamera.value?.stopRecording(() => {
192
197
  if (recordCamera.value) {
193
198
  const blob = recorderCamera.value!.getBlob()
194
- const url = URL.createObjectURL(blob)
195
- download(getFilename('camera', config.mimeType), url)
196
- window.URL.revokeObjectURL(url)
199
+ downloadBlob(blob, duration, getFilename('camera', config.mimeType))
197
200
  }
198
201
  recorderCamera.value = undefined
199
202
  if (!showAvatar.value)
@@ -201,15 +204,20 @@ export function useRecording() {
201
204
  })
202
205
  recorderSlides.value?.stopRecording(() => {
203
206
  const blob = recorderSlides.value!.getBlob()
204
- const url = URL.createObjectURL(blob)
205
- download(getFilename('screen', config.mimeType), url)
206
- window.URL.revokeObjectURL(url)
207
+ downloadBlob(blob, duration, getFilename('screen', config.mimeType))
207
208
  closeStream(streamCapture)
208
209
  closeStream(streamSlides)
209
210
  recorderSlides.value = undefined
210
211
  })
211
212
  }
212
213
 
214
+ async function downloadBlob(blob: Blob, duration: number, filename: string) {
215
+ const fixedBlob = await fixWebmDuration(blob, duration, { logger: false })
216
+ const url = URL.createObjectURL(fixedBlob)
217
+ download(filename, url)
218
+ window.URL.revokeObjectURL(url)
219
+ }
220
+
213
221
  function closeStream(stream: Ref<MediaStream | undefined>) {
214
222
  const s = stream.value
215
223
  if (!s)
package/logic/slides.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { SlideRoute } from '@slidev/types'
2
2
  import { slides } from '#slidev/slides'
3
- import { computed, watch, watchEffect } from 'vue'
4
- import { useNav } from '../composables/useNav'
3
+ import { tryOnMounted } from '@vueuse/core'
4
+ import { computed, watch } from 'vue'
5
5
  import { useSlideContext } from '../context'
6
6
 
7
7
  export { slides }
@@ -24,17 +24,29 @@ export function getSlidePath(
24
24
  }
25
25
 
26
26
  export function useIsSlideActive() {
27
- const { $page } = useSlideContext()
28
- const { currentSlideNo } = useNav()
29
- return computed(() => $page.value === currentSlideNo.value)
27
+ const { $page, $nav } = useSlideContext()
28
+ // Use `$nav.value.currentSlideNo` rather than `useNav().currentSlideNo` to make it work in print/export mode. See https://github.com/slidevjs/slidev/issues/2310.
29
+ return computed(() => $page.value === $nav.value.currentSlideNo)
30
30
  }
31
31
 
32
- export function onSlideEnter(cb: () => void) {
33
- const isActive = useIsSlideActive()
34
- watchEffect(() => isActive.value && cb())
32
+ export function onSlideEnter(cb: (to: number, from: number | undefined) => any) {
33
+ const { $page, $nav } = useSlideContext()
34
+
35
+ tryOnMounted(() => {
36
+ watch(() => $nav.value.currentSlideNo, (to, from) => {
37
+ if ($page.value === to)
38
+ cb(to, from)
39
+ }, { immediate: true })
40
+ })
35
41
  }
36
42
 
37
- export function onSlideLeave(cb: () => void) {
38
- const isActive = useIsSlideActive()
39
- watch(isActive, () => !isActive.value && cb())
43
+ export function onSlideLeave(cb: (to: number, from: number | undefined) => any) {
44
+ const { $page, $nav } = useSlideContext()
45
+
46
+ tryOnMounted(() => {
47
+ watch(() => $nav.value.currentSlideNo, (to, from) => {
48
+ if ($page.value === from)
49
+ cb(to, from)
50
+ })
51
+ })
40
52
  }
@@ -2,16 +2,29 @@ import type { ClicksElement, RawAtValue } from '@slidev/types'
2
2
  import type { App, DirectiveBinding } from 'vue'
3
3
  import { computed, watchEffect } from 'vue'
4
4
  import {
5
+ CLASS_VCLICK_ANIMATION_PREFIX,
5
6
  CLASS_VCLICK_CURRENT,
6
- CLASS_VCLICK_FADE,
7
7
  CLASS_VCLICK_HIDDEN,
8
8
  CLASS_VCLICK_HIDDEN_EXP,
9
9
  CLASS_VCLICK_PRIOR,
10
10
  CLASS_VCLICK_TARGET,
11
11
  injectionClicksContext,
12
+ injectionFrontmatter,
12
13
  } from '../constants'
14
+ import { configs } from '../env'
13
15
  import { directiveInject } from '../utils'
14
16
 
17
+ const RE_WHITESPACE_OR_COMMA = /[\s,]+/
18
+
19
+ function syncAnimationClasses(el: HTMLElement, animations: string[]) {
20
+ const targetClasses = animations.map(a => `${CLASS_VCLICK_ANIMATION_PREFIX}${a}`)
21
+ el.classList.forEach((c) => {
22
+ if (c.startsWith(CLASS_VCLICK_ANIMATION_PREFIX) && !targetClasses.includes(c))
23
+ el.classList.remove(c)
24
+ })
25
+ targetClasses.forEach(c => el.classList.add(c))
26
+ }
27
+
15
28
  export function createVClickDirectives() {
16
29
  return {
17
30
  install(app: App) {
@@ -38,13 +51,15 @@ export function createVClickDirectives() {
38
51
  const prior = active && !current
39
52
 
40
53
  if (resolved.flagHide) {
41
- el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, active)
54
+ el.classList.toggle(CLASS_VCLICK_HIDDEN, active)
42
55
  el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
43
56
  }
44
57
  else {
45
- el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, !active)
58
+ el.classList.toggle(CLASS_VCLICK_HIDDEN, !active)
46
59
  }
47
60
 
61
+ syncAnimationClasses(el, resolved.flagAnimations.value)
62
+
48
63
  el.classList.toggle(CLASS_VCLICK_CURRENT, current)
49
64
  el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
50
65
  })
@@ -70,13 +85,15 @@ export function createVClickDirectives() {
70
85
  const prior = active && !current
71
86
 
72
87
  if (resolved.flagHide) {
73
- el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, active)
88
+ el.classList.toggle(CLASS_VCLICK_HIDDEN, active)
74
89
  el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
75
90
  }
76
91
  else {
77
- el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, !active)
92
+ el.classList.toggle(CLASS_VCLICK_HIDDEN, !active)
78
93
  }
79
94
 
95
+ syncAnimationClasses(el, resolved.flagAnimations.value)
96
+
80
97
  el.classList.toggle(CLASS_VCLICK_CURRENT, current)
81
98
  el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
82
99
  })
@@ -101,9 +118,11 @@ export function createVClickDirectives() {
101
118
  const current = resolved.isCurrent.value
102
119
  const prior = active && !current
103
120
 
104
- el.classList.toggle(resolved.flagFade ? CLASS_VCLICK_FADE : CLASS_VCLICK_HIDDEN, active)
121
+ el.classList.toggle(CLASS_VCLICK_HIDDEN, active)
105
122
  el.classList.toggle(CLASS_VCLICK_HIDDEN_EXP, active)
106
123
 
124
+ syncAnimationClasses(el, resolved.flagAnimations.value)
125
+
107
126
  el.classList.toggle(CLASS_VCLICK_CURRENT, current)
108
127
  el.classList.toggle(CLASS_VCLICK_PRIOR, prior)
109
128
  })
@@ -118,12 +137,27 @@ export const resolvedClickMap = new Map<ClicksElement, ReturnType<typeof resolve
118
137
 
119
138
  export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, value: RawAtValue, explicitHide = false) {
120
139
  const ctx = directiveInject(dir, injectionClicksContext)?.value
140
+ const frontmatter = directiveInject(dir, injectionFrontmatter)
121
141
 
122
142
  if (!el || !ctx)
123
143
  return null
124
144
 
125
145
  const flagHide = explicitHide || (dir.modifiers.hide !== false && dir.modifiers.hide != null)
126
- const flagFade = dir.modifiers.fade !== false && dir.modifiers.fade != null
146
+
147
+ /**
148
+ * Resolves the animation presets for this element.
149
+ * Priority: directive modifiers (stacked) > slide frontmatter > global config.
150
+ * Modifiers allow composition, e.g., v-click.fade.up.scale.
151
+ */
152
+ const elModifiers = Object.keys({ ...dir.modifiers }).filter(m => m !== 'hide')
153
+ const flagAnimations = computed(() => {
154
+ if (elModifiers.length > 0)
155
+ return elModifiers
156
+ const preset = frontmatter?.clickAnimation || configs.clickAnimation
157
+ if (preset)
158
+ return preset.split(RE_WHITESPACE_OR_COMMA).filter(Boolean)
159
+ return []
160
+ })
127
161
 
128
162
  const info = ctx.calculate(value)
129
163
  if (!info)
@@ -145,8 +179,8 @@ export function resolveClick(el: Element | string, dir: DirectiveBinding<any>, v
145
179
  ...info,
146
180
  isShown,
147
181
  visibilityState,
148
- flagFade,
149
182
  flagHide,
183
+ flagAnimations,
150
184
  }
151
185
  resolvedClickMap.set(el, resolved)
152
186
  return resolved
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
3
  "type": "module",
4
- "version": "52.14.1",
4
+ "version": "52.15.0",
5
5
  "description": "Presentation slides for developers",
6
6
  "author": "Anthony Fu <anthonyfu117@hotmail.com>",
7
7
  "license": "MIT",
@@ -25,48 +25,49 @@
25
25
  },
26
26
  "main": "./public.ts",
27
27
  "engines": {
28
- "node": ">=18.0.0"
28
+ "node": ">=20.12.0"
29
29
  },
30
30
  "dependencies": {
31
31
  "@antfu/utils": "^9.3.0",
32
- "@iconify-json/carbon": "^1.2.19",
32
+ "@iconify-json/carbon": "^1.2.20",
33
33
  "@iconify-json/ph": "^1.2.2",
34
34
  "@iconify-json/svg-spinners": "^1.2.4",
35
- "@shikijs/engine-javascript": "^4.0.1",
36
- "@shikijs/monaco": "^4.0.1",
37
- "@shikijs/vitepress-twoslash": "^4.0.1",
35
+ "@shikijs/engine-javascript": "^4.0.2",
36
+ "@shikijs/monaco": "^4.0.2",
37
+ "@shikijs/vitepress-twoslash": "^4.0.2",
38
38
  "@slidev/rough-notation": "^0.1.0",
39
39
  "@typescript/ata": "^0.9.8",
40
- "@unhead/vue": "^2.1.10",
41
- "@unocss/extractor-mdc": "^66.6.3",
42
- "@unocss/preset-mini": "^66.6.3",
43
- "@unocss/reset": "^66.6.3",
40
+ "@unhead/vue": "^2.1.13",
41
+ "@unocss/extractor-mdc": "^66.6.8",
42
+ "@unocss/preset-mini": "^66.6.8",
43
+ "@unocss/reset": "^66.6.8",
44
44
  "@vueuse/core": "^14.2.1",
45
45
  "@vueuse/math": "^14.2.1",
46
46
  "@vueuse/motion": "^3.0.3",
47
47
  "ansis": "^4.2.0",
48
48
  "drauu": "^1.0.0",
49
49
  "file-saver": "^2.0.5",
50
+ "fix-webm-duration": "^1.0.6",
50
51
  "floating-vue": "^5.2.2",
51
- "fuse.js": "^7.1.0",
52
- "katex": "^0.16.33",
52
+ "fuse.js": "^7.3.0",
53
+ "katex": "^0.16.45",
53
54
  "lz-string": "^1.5.0",
54
- "mermaid": "^11.12.3",
55
+ "mermaid": "^11.14.0",
55
56
  "monaco-editor": "^0.55.1",
56
57
  "nanotar": "^0.3.0",
57
58
  "pptxgenjs": "^4.0.1",
58
59
  "recordrtc": "^5.6.2",
59
- "shiki": "^4.0.1",
60
- "shiki-magic-move": "^1.2.1",
60
+ "shiki": "^4.0.2",
61
+ "shiki-magic-move": "^1.3.0",
61
62
  "typescript": "^5.9.3",
62
- "unocss": "^66.6.2",
63
- "vue": "^3.5.29",
64
- "vue-router": "^5.0.3",
65
- "yaml": "^2.8.2",
66
- "@slidev/parser": "52.14.1",
67
- "@slidev/types": "52.14.1"
63
+ "unocss": "^66.6.8",
64
+ "vue": "^3.5.33",
65
+ "vue-router": "^5.0.6",
66
+ "yaml": "^2.8.3",
67
+ "@slidev/parser": "52.15.0",
68
+ "@slidev/types": "52.15.0"
68
69
  },
69
70
  "devDependencies": {
70
- "vite": "^7.3.1"
71
+ "vite": "^8.0.10"
71
72
  }
72
73
  }
package/pages/404.vue CHANGED
@@ -3,12 +3,14 @@ import { computed } from 'vue'
3
3
  import { useRouter } from 'vue-router'
4
4
  import { useNav } from '../composables/useNav'
5
5
 
6
+ const RE_DIGITS = /\d+/
7
+
6
8
  const { currentRoute } = useRouter()
7
9
  const { total } = useNav()
8
10
 
9
11
  const guessedSlide = computed(() => {
10
12
  const path = currentRoute.value.path
11
- const match = path.match(/\d+/)
13
+ const match = path.match(RE_DIGITS)
12
14
  if (match) {
13
15
  const slideNo = +match[0]
14
16
  if (slideNo > 0 && slideNo <= total.value)
package/pages/export.vue CHANGED
@@ -161,7 +161,7 @@ async function pngsGz() {
161
161
  })),
162
162
  )
163
163
  const a = document.createElement('a')
164
- const blob = new Blob([data], { type: 'application/gzip' })
164
+ const blob = new Blob([data as any], { type: 'application/gzip' })
165
165
  a.href = URL.createObjectURL(blob)
166
166
  a.download = `${title.value}.tar.gz`
167
167
  a.click()
@@ -351,6 +351,10 @@ h2 {
351
351
  #export-content {
352
352
  --uno: pointer-events-none;
353
353
  }
354
+
355
+ #export-container {
356
+ --slidev-slide-container-scale: var(--slidev-slide-scale);
357
+ }
354
358
  </style>
355
359
 
356
360
  <style>
@@ -9,6 +9,8 @@ import { slidesTitle } from '../env'
9
9
  import IconButton from '../internals/IconButton.vue'
10
10
  import Modal from '../internals/Modal.vue'
11
11
 
12
+ const RE_SLIDE_HEADER = /^---\s*#(\d+)\s*$/
13
+
12
14
  useHead({ title: `Notes Edit - ${slidesTitle}` })
13
15
 
14
16
  const { slides } = useNav()
@@ -35,7 +37,7 @@ function deserializeNotes(notes: string, slides: SlideRoute[]) {
35
37
  const lines = notes.split(/^(---\s*#\d+\s*)$/gm)
36
38
 
37
39
  lines.forEach((line, index) => {
38
- const match = line.match(/^---\s*#(\d+)\s*$/)
40
+ const match = line.match(RE_SLIDE_HEADER)
39
41
  if (match) {
40
42
  const no = Number.parseInt(match[1])
41
43
  const note = lines[index + 1].trim()
package/pages/play.vue CHANGED
@@ -7,6 +7,7 @@ import { useNav } from '../composables/useNav'
7
7
  import { useSwipeControls } from '../composables/useSwipeControls'
8
8
  import { useWakeLock } from '../composables/useWakeLock'
9
9
  import Controls from '../internals/Controls.vue'
10
+ import LaserPointer from '../internals/LaserPointer.vue'
10
11
  import NavControls from '../internals/NavControls.vue'
11
12
  import PresenterMouse from '../internals/PresenterMouse.vue'
12
13
  import SlideContainer from '../internals/SlideContainer.vue'
@@ -97,6 +98,7 @@ const contentStyle = computed(() => {
97
98
  <template #default>
98
99
  <SlidesShow render-context="slide" />
99
100
  <PresenterMouse />
101
+ <LaserPointer />
100
102
  </template>
101
103
  <template #controls>
102
104
  <div
@@ -1,9 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  import { useHead } from '@unhead/vue'
3
- import { useEventListener, useLocalStorage, useMediaQuery, useMouse, useWindowFocus } from '@vueuse/core'
4
- import { computed, onMounted, reactive, ref, shallowRef, watch } from 'vue'
3
+ import { useEventListener, useLocalStorage, useMediaQuery, useWindowFocus } from '@vueuse/core'
4
+ import { computed, onMounted, ref, shallowRef, watch, watchEffect } from 'vue'
5
5
  import { createClicksContextBase } from '../composables/useClicks'
6
6
  import { useDrawings } from '../composables/useDrawings'
7
+ import { useMousePosInSlide } from '../composables/useMousePosInSlide'
7
8
  import { useNav } from '../composables/useNav'
8
9
  import { useSwipeControls } from '../composables/useSwipeControls'
9
10
  import { useWakeLock } from '../composables/useWakeLock'
@@ -14,6 +15,7 @@ import CurrentProgressBar from '../internals/CurrentProgressBar.vue'
14
15
  import DrawingControls from '../internals/DrawingControls.vue'
15
16
  import Goto from '../internals/Goto.vue'
16
17
  import IconButton from '../internals/IconButton.vue'
18
+ import LaserPointer from '../internals/LaserPointer.vue'
17
19
  import NavControls from '../internals/NavControls.vue'
18
20
  import NoteEditable from '../internals/NoteEditable.vue'
19
21
  import NoteStatic from '../internals/NoteStatic.vue'
@@ -27,7 +29,7 @@ import TimerBar from '../internals/TimerBar.vue'
27
29
  import TimerInlined from '../internals/TimerInlined.vue'
28
30
  import { onContextMenu } from '../logic/contextMenu'
29
31
  import { registerShortcuts } from '../logic/shortcuts'
30
- import { decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state'
32
+ import { cursorStyle, decreasePresenterFontSize, increasePresenterFontSize, presenterLayout, presenterNotesFontSize, showEditor, showPresenterCursor } from '../state'
31
33
  import { sharedState } from '../state/shared'
32
34
 
33
35
  const inFocus = useWindowFocus()
@@ -244,28 +246,20 @@ if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
244
246
 
245
247
  // sync presenter cursor
246
248
  onMounted(() => {
247
- const slidesContainer = main.value!.querySelector('#slide-content')!
248
- const mouse = reactive(useMouse())
249
+ const mouse = useMousePosInSlide()
249
250
  const focus = useWindowFocus()
250
251
 
251
- watch(
252
- () => {
253
- if (!focus.value || isDrawing.value || !showPresenterCursor.value || !slidesContainer)
254
- return undefined
255
-
256
- const rect = slidesContainer.getBoundingClientRect()
257
- const x = (mouse.x - rect.left) / rect.width * 100
258
- const y = (mouse.y - rect.top) / rect.height * 100
259
-
260
- if (x < 0 || x > 100 || y < 0 || y > 100)
261
- return undefined
262
-
263
- return { x, y }
264
- },
265
- (pos) => {
266
- sharedState.cursor = pos
267
- },
268
- )
252
+ watchEffect(() => {
253
+ if (!mouse.value || !focus.value || isDrawing.value || !showPresenterCursor.value) {
254
+ sharedState.cursor = undefined
255
+ }
256
+ else {
257
+ sharedState.cursor = {
258
+ ...mouse.value,
259
+ style: cursorStyle.value,
260
+ }
261
+ }
262
+ })
269
263
  })
270
264
  </script>
271
265
 
@@ -328,6 +322,7 @@ onMounted(() => {
328
322
  @contextmenu="onContextMenu"
329
323
  >
330
324
  <SlidesShow render-context="presenter" />
325
+ <LaserPointer />
331
326
  </SlideContainer>
332
327
 
333
328
  <ClicksSlider
package/setup/main.ts CHANGED
@@ -9,8 +9,7 @@ import { createVDragDirective } from '../modules/v-drag'
9
9
  import { createVMarkDirective } from '../modules/v-mark'
10
10
  import { createVMotionDirectives } from '../modules/v-motion'
11
11
  import setupRoutes from '../setup/routes'
12
-
13
- import '#slidev/styles'
12
+ import '../styles'
14
13
 
15
14
  export default async function setupMain(app: App) {
16
15
  function setMaxHeight() {
package/setup/monaco.ts CHANGED
@@ -56,12 +56,12 @@ StandaloneServices.initialize({
56
56
  })
57
57
 
58
58
  const setup = createSingletonPromise(async () => {
59
- const defaults = monaco.languages.typescript.typescriptDefaults
59
+ const defaults = monaco.typescript.typescriptDefaults
60
60
  defaults.setCompilerOptions({
61
61
  ...defaults.getCompilerOptions(),
62
62
  strict: true,
63
- moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
64
- module: monaco.languages.typescript.ModuleKind.ESNext,
63
+ moduleResolution: monaco.typescript.ModuleResolutionKind.NodeJs,
64
+ module: monaco.typescript.ModuleKind.ESNext,
65
65
  })
66
66
 
67
67
  const ata = configs.monacoTypesSource === 'cdn'
@@ -137,7 +137,7 @@ const setup = createSingletonPromise(async () => {
137
137
  async function _addFile(raw: () => Promise<{ default: string }>, path: string) {
138
138
  const uri = monaco.Uri.file(path)
139
139
  const code = (await raw()).default
140
- monaco.languages.typescript.typescriptDefaults.addExtraLib(code, `file:///${path}`)
140
+ monaco.typescript.typescriptDefaults.addExtraLib(code, `file:///${path}`)
141
141
  monaco.editor.createModel(code, 'javascript', uri)
142
142
  }
143
143
 
package/setup/root.ts CHANGED
@@ -80,7 +80,7 @@ export default function setupRoot() {
80
80
  patch('lastUpdate', {
81
81
  id,
82
82
  type: syncType.value,
83
- time: new Date().getTime(),
83
+ time: Date.now(),
84
84
  })
85
85
  }
86
86
  const router = useRouter()
package/shim-vue.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  declare module 'vue' {
2
- type SlideContext = import('./context').SlideContext
2
+ import type { ShallowUnwrapRef } from 'vue'
3
+
4
+ type SlideContext = ShallowUnwrapRef<import('./context').SlideContext>
3
5
  interface ComponentCustomProperties extends SlideContext {
4
6
  }
5
7
  }
package/state/shared.ts CHANGED
@@ -20,6 +20,7 @@ export interface SharedState {
20
20
  cursor?: {
21
21
  x: number
22
22
  y: number
23
+ style?: 'cursor' | 'laser'
23
24
  }
24
25
 
25
26
  lastUpdate?: {