@slidev/client 52.14.2 → 52.15.1
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/builtin/BlueSky.vue +167 -0
- package/builtin/CodeBlockWrapper.vue +1 -1
- package/builtin/Link.vue +2 -2
- package/builtin/Monaco.vue +1 -2
- package/builtin/Tweet.vue +1 -1
- package/builtin/VClick.ts +8 -0
- package/builtin/VClicks.ts +24 -4
- package/builtin/VDrag.vue +1 -0
- package/composables/useDragElements.ts +5 -2
- package/composables/useDrawings.ts +6 -6
- package/composables/useMousePosInSlide.ts +21 -0
- package/composables/usePreloadImages.ts +2 -1
- package/composables/useSlideInfo.ts +1 -2
- package/constants.ts +2 -1
- package/internals/DrawingControls.vue +25 -7
- package/internals/IconButton.vue +3 -1
- package/internals/LaserPointer.vue +66 -0
- package/internals/MenuButton.vue +1 -1
- package/internals/NavControls.vue +28 -18
- package/internals/PresenterMouse.vue +1 -1
- package/internals/Settings.vue +10 -1
- package/internals/ShikiEditor.vue +2 -4
- package/internals/SideEditor.vue +3 -1
- package/internals/SlideContainer.vue +18 -1
- package/logic/recording.ts +16 -8
- package/logic/slides.ts +20 -7
- package/modules/v-click.ts +42 -8
- package/package.json +22 -21
- package/pages/404.vue +3 -1
- package/pages/export.vue +5 -1
- package/pages/notes-edit.vue +3 -1
- package/pages/play.vue +2 -0
- package/pages/presenter.vue +18 -23
- package/setup/main.ts +1 -2
- package/setup/monaco.ts +4 -4
- package/setup/root.ts +1 -1
- package/shim-vue.d.ts +3 -1
- package/state/shared.ts +1 -0
- package/state/storage.ts +7 -0
- package/styles/index.css +8 -22
- package/styles/index.ts +23 -0
- package/styles/layouts-base.css +1 -1
- package/styles/vclick.css +54 -0
|
@@ -5,7 +5,7 @@ import { useDrawings } from '../composables/useDrawings'
|
|
|
5
5
|
import { useNav } from '../composables/useNav'
|
|
6
6
|
import { configs } from '../env'
|
|
7
7
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
8
|
-
import { activeElement, breakpoints, fullscreen, hasViewerCssFilter, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
8
|
+
import { activeElement, breakpoints, cursorStyle, fullscreen, hasViewerCssFilter, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterCursor, togglePresenterLayout } from '../state'
|
|
9
9
|
import { downloadPDF } from '../utils'
|
|
10
10
|
import IconButton from './IconButton.vue'
|
|
11
11
|
import MenuButton from './MenuButton.vue'
|
|
@@ -67,10 +67,10 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
67
67
|
<div v-if="isFullscreen" class="i-carbon:minimize" />
|
|
68
68
|
<div v-else class="i-carbon:maximize" />
|
|
69
69
|
</IconButton>
|
|
70
|
-
<IconButton :
|
|
70
|
+
<IconButton :disabled="!hasPrev" title="Go to previous slide" @click="prev">
|
|
71
71
|
<div class="i-carbon:arrow-left" />
|
|
72
72
|
</IconButton>
|
|
73
|
-
<IconButton :
|
|
73
|
+
<IconButton :disabled="!hasNext" title="Go to next slide" @click="next">
|
|
74
74
|
<div class="i-carbon:arrow-right" />
|
|
75
75
|
</IconButton>
|
|
76
76
|
<IconButton v-if="!isEmbedded" title="Show slide overview" @click="toggleOverview()">
|
|
@@ -81,8 +81,8 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
81
81
|
:title="isDark ? 'Switch to light mode theme' : 'Switch to dark mode theme'"
|
|
82
82
|
@click="toggleDark()"
|
|
83
83
|
>
|
|
84
|
-
<
|
|
85
|
-
<
|
|
84
|
+
<div v-if="isDark" class="i-carbon-moon" />
|
|
85
|
+
<div v-else class="i-carbon-sun" />
|
|
86
86
|
</IconButton>
|
|
87
87
|
|
|
88
88
|
<VerticalDivider />
|
|
@@ -95,16 +95,28 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
95
95
|
|
|
96
96
|
<IconButton
|
|
97
97
|
v-if="isPresenter"
|
|
98
|
-
:title="
|
|
99
|
-
|
|
98
|
+
:title="cursorStyle === 'laser'
|
|
99
|
+
? (showPresenterCursor ? 'Disable laser pointer' : 'Enable laser pointer')
|
|
100
|
+
: (showPresenterCursor ? 'Hide presenter cursor' : 'Show presenter cursor')"
|
|
101
|
+
:active="showPresenterCursor"
|
|
102
|
+
@click="togglePresenterCursor()"
|
|
100
103
|
>
|
|
101
|
-
<
|
|
102
|
-
|
|
104
|
+
<template v-if="cursorStyle === 'laser'">
|
|
105
|
+
<div v-if="showPresenterCursor" class="i-carbon-magic-wand-filled color-red-600 dark:color-red-400" />
|
|
106
|
+
<div v-else class="i-carbon-magic-wand" />
|
|
107
|
+
</template>
|
|
108
|
+
<template v-else>
|
|
109
|
+
<div v-if="showPresenterCursor" class="i-ph-cursor-fill" />
|
|
110
|
+
<div v-else class="i-ph-cursor-duotone" />
|
|
111
|
+
</template>
|
|
103
112
|
</IconButton>
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
<IconButton
|
|
114
|
+
v-if="__SLIDEV_FEATURE_DRAWINGS__ && (!configs.drawings.presenterOnly || isPresenter)"
|
|
115
|
+
class="relative"
|
|
116
|
+
:title="drawingEnabled ? 'Hide drawing toolbar' : 'Show drawing toolbar'"
|
|
117
|
+
:active="drawingEnabled"
|
|
118
|
+
@click="drawingEnabled = !drawingEnabled"
|
|
119
|
+
>
|
|
108
120
|
<div class="i-carbon:pen" />
|
|
109
121
|
<div
|
|
110
122
|
v-if="drawingEnabled"
|
|
@@ -112,17 +124,15 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
112
124
|
:style="{ background: brush.color }"
|
|
113
125
|
/>
|
|
114
126
|
</IconButton>
|
|
127
|
+
|
|
115
128
|
<VerticalDivider />
|
|
116
|
-
</template>
|
|
117
129
|
|
|
118
|
-
<template v-if="!isEmbedded">
|
|
119
130
|
<IconButton v-if="isPresenter" title="Play Mode" @click="exitPresenter">
|
|
120
131
|
<div class="i-carbon:presentation-file" />
|
|
121
132
|
</IconButton>
|
|
122
133
|
<IconButton v-if="__SLIDEV_FEATURE_PRESENTER__ && isPresenterAvailable" title="Presenter Mode" @click="enterPresenter">
|
|
123
134
|
<div class="i-carbon:user-speaker" />
|
|
124
135
|
</IconButton>
|
|
125
|
-
|
|
126
136
|
<IconButton
|
|
127
137
|
v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__"
|
|
128
138
|
:title="showEditor ? 'Hide editor' : 'Show editor'"
|
|
@@ -164,8 +174,8 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
164
174
|
<SyncControls v-if="__SLIDEV_FEATURE_PRESENTER__" />
|
|
165
175
|
|
|
166
176
|
<MenuButton>
|
|
167
|
-
<template #button>
|
|
168
|
-
<IconButton title="More Options">
|
|
177
|
+
<template #button="{ value }">
|
|
178
|
+
<IconButton title="More Options" :active="value">
|
|
169
179
|
<div class="i-carbon:settings-adjust" />
|
|
170
180
|
<div v-if="hasViewerCssFilter" w-2 h-2 bg-primary rounded-full absolute top-0.5 right-0.5 />
|
|
171
181
|
</IconButton>
|
package/internals/Settings.vue
CHANGED
|
@@ -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 {
|
|
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)} `" />
|
|
28
26
|
<textarea
|
|
29
|
-
|
|
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"
|
package/internals/SideEditor.vue
CHANGED
|
@@ -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(
|
|
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 {
|
|
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"
|
package/logic/recording.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type RecorderType from 'recordrtc'
|
|
|
2
2
|
import type { Options as RecorderOptions } from 'recordrtc'
|
|
3
3
|
import type { Ref } from 'vue'
|
|
4
4
|
import { isTruthy } from '@antfu/utils'
|
|
5
|
+
import { fixWebmDuration } from '@fix-webm-duration/fix'
|
|
5
6
|
import { useDevicesList, useEventListener, useLocalStorage } from '@vueuse/core'
|
|
6
7
|
import { nextTick, ref, shallowRef, watch } from 'vue'
|
|
7
8
|
import { currentCamera, currentMic } from '../state'
|
|
@@ -50,11 +51,11 @@ export const {
|
|
|
50
51
|
} = useDevicesList({
|
|
51
52
|
onUpdated() {
|
|
52
53
|
if (currentCamera.value !== 'none') {
|
|
53
|
-
if (!cameras.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.
|
|
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
|
-
|
|
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
|
-
|
|
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,6 +1,7 @@
|
|
|
1
1
|
import type { SlideRoute } from '@slidev/types'
|
|
2
2
|
import { slides } from '#slidev/slides'
|
|
3
|
-
import {
|
|
3
|
+
import { tryOnMounted } from '@vueuse/core'
|
|
4
|
+
import { computed, watch } from 'vue'
|
|
4
5
|
import { useSlideContext } from '../context'
|
|
5
6
|
|
|
6
7
|
export { slides }
|
|
@@ -28,12 +29,24 @@ export function useIsSlideActive() {
|
|
|
28
29
|
return computed(() => $page.value === $nav.value.currentSlideNo)
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
export function onSlideEnter(cb: () =>
|
|
32
|
-
const
|
|
33
|
-
|
|
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
|
+
})
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
export function onSlideLeave(cb: () =>
|
|
37
|
-
const
|
|
38
|
-
|
|
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
|
+
})
|
|
39
52
|
}
|
package/modules/v-click.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "52.15.1",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,18 +29,19 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@antfu/utils": "^9.3.0",
|
|
32
|
-
"@
|
|
32
|
+
"@fix-webm-duration/fix": "^1.0.1",
|
|
33
|
+
"@iconify-json/carbon": "^1.2.20",
|
|
33
34
|
"@iconify-json/ph": "^1.2.2",
|
|
34
35
|
"@iconify-json/svg-spinners": "^1.2.4",
|
|
35
|
-
"@shikijs/engine-javascript": "^4.0.
|
|
36
|
-
"@shikijs/monaco": "^4.0.
|
|
37
|
-
"@shikijs/vitepress-twoslash": "^4.0.
|
|
36
|
+
"@shikijs/engine-javascript": "^4.0.2",
|
|
37
|
+
"@shikijs/monaco": "^4.0.2",
|
|
38
|
+
"@shikijs/vitepress-twoslash": "^4.0.2",
|
|
38
39
|
"@slidev/rough-notation": "^0.1.0",
|
|
39
40
|
"@typescript/ata": "^0.9.8",
|
|
40
|
-
"@unhead/vue": "^2.1.
|
|
41
|
-
"@unocss/extractor-mdc": "^66.6.
|
|
42
|
-
"@unocss/preset-mini": "^66.6.
|
|
43
|
-
"@unocss/reset": "^66.6.
|
|
41
|
+
"@unhead/vue": "^2.1.13",
|
|
42
|
+
"@unocss/extractor-mdc": "^66.6.8",
|
|
43
|
+
"@unocss/preset-mini": "^66.6.8",
|
|
44
|
+
"@unocss/reset": "^66.6.8",
|
|
44
45
|
"@vueuse/core": "^14.2.1",
|
|
45
46
|
"@vueuse/math": "^14.2.1",
|
|
46
47
|
"@vueuse/motion": "^3.0.3",
|
|
@@ -48,25 +49,25 @@
|
|
|
48
49
|
"drauu": "^1.0.0",
|
|
49
50
|
"file-saver": "^2.0.5",
|
|
50
51
|
"floating-vue": "^5.2.2",
|
|
51
|
-
"fuse.js": "^7.
|
|
52
|
-
"katex": "^0.16.
|
|
52
|
+
"fuse.js": "^7.3.0",
|
|
53
|
+
"katex": "^0.16.45",
|
|
53
54
|
"lz-string": "^1.5.0",
|
|
54
|
-
"mermaid": "^11.
|
|
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.
|
|
60
|
-
"shiki-magic-move": "^1.
|
|
60
|
+
"shiki": "^4.0.2",
|
|
61
|
+
"shiki-magic-move": "^1.3.0",
|
|
61
62
|
"typescript": "^5.9.3",
|
|
62
|
-
"unocss": "^66.6.
|
|
63
|
-
"vue": "^3.5.
|
|
64
|
-
"vue-router": "^5.0.
|
|
65
|
-
"yaml": "^2.8.
|
|
66
|
-
"@slidev/
|
|
67
|
-
"@slidev/
|
|
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.1",
|
|
68
|
+
"@slidev/types": "52.15.1"
|
|
68
69
|
},
|
|
69
70
|
"devDependencies": {
|
|
70
|
-
"vite": "^
|
|
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(
|
|
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>
|
package/pages/notes-edit.vue
CHANGED
|
@@ -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(
|
|
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
|