@slidev/client 52.14.2 → 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.
- 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 +2 -2
- 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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
A simple wrapper for embedded Bluesky posts
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
|
|
6
|
+
<BlueSky uri="at://did:plc:432mbsu2xucyvxl6sluohidu/app.bsky.feed.post/3lgwg2vy44s2q" />
|
|
7
|
+
<BlueSky uri="https://bsky.app/profile/sli.dev/post/3lgwg2vy44s2q" />
|
|
8
|
+
-->
|
|
9
|
+
|
|
10
|
+
<script setup lang="ts">
|
|
11
|
+
import { nextTick, onMounted, ref } from 'vue'
|
|
12
|
+
import { isDark } from '../logic/dark'
|
|
13
|
+
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
uri: string
|
|
16
|
+
scale?: string | number
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const RE_BLUESKY_POST_URL = /^\/profile\/([^/]+)\/post\/([^/?#]+)$/
|
|
20
|
+
|
|
21
|
+
const container = ref<HTMLElement | null>(null)
|
|
22
|
+
const loaded = ref(false)
|
|
23
|
+
const postNotFound = ref(false)
|
|
24
|
+
const resolvedUri = ref('')
|
|
25
|
+
const resolvedPostUrl = ref('')
|
|
26
|
+
|
|
27
|
+
const RESOLVE_HANDLE_URL = 'https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle'
|
|
28
|
+
const GET_POST_THREAD_URL = 'https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread'
|
|
29
|
+
|
|
30
|
+
interface BlueskyThreadResponse {
|
|
31
|
+
thread?: {
|
|
32
|
+
post?: {
|
|
33
|
+
uri?: string
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scan(retries = 10) {
|
|
39
|
+
// @ts-expect-error global
|
|
40
|
+
if (!window.bluesky?.scan) {
|
|
41
|
+
if (retries <= 0)
|
|
42
|
+
console.error('Failed to load Bluesky embed widget after 10 retries.')
|
|
43
|
+
if (retries <= 0)
|
|
44
|
+
return
|
|
45
|
+
setTimeout(scan, 1000, retries - 1)
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
// @ts-expect-error global
|
|
49
|
+
window.bluesky.scan(container.value || undefined)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractPostInfoFromUrl(input: string) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(input)
|
|
55
|
+
const match = url.pathname.match(RE_BLUESKY_POST_URL)
|
|
56
|
+
if (!match)
|
|
57
|
+
return
|
|
58
|
+
return {
|
|
59
|
+
handle: decodeURIComponent(match[1]),
|
|
60
|
+
postId: decodeURIComponent(match[2]),
|
|
61
|
+
postUrl: url.toString(),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function resolvePostUri(input: string) {
|
|
68
|
+
try {
|
|
69
|
+
if (input.startsWith('at://')) {
|
|
70
|
+
resolvedUri.value = input
|
|
71
|
+
resolvedPostUrl.value = ''
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const info = extractPostInfoFromUrl(input)
|
|
76
|
+
if (!info)
|
|
77
|
+
return false
|
|
78
|
+
|
|
79
|
+
const handleResponse = await fetch(`${RESOLVE_HANDLE_URL}?handle=${encodeURIComponent(info.handle)}`)
|
|
80
|
+
if (!handleResponse.ok)
|
|
81
|
+
return false
|
|
82
|
+
const handleData = await handleResponse.json() as { did?: string }
|
|
83
|
+
if (!handleData.did)
|
|
84
|
+
return false
|
|
85
|
+
|
|
86
|
+
const uri = `at://${handleData.did}/app.bsky.feed.post/${info.postId}`
|
|
87
|
+
const threadResponse = await fetch(
|
|
88
|
+
`${GET_POST_THREAD_URL}?uri=${encodeURIComponent(uri)}&depth=0&parentHeight=0`,
|
|
89
|
+
)
|
|
90
|
+
if (!threadResponse.ok)
|
|
91
|
+
return false
|
|
92
|
+
|
|
93
|
+
const threadData = await threadResponse.json() as BlueskyThreadResponse
|
|
94
|
+
const canonicalUri = threadData.thread?.post?.uri
|
|
95
|
+
if (!canonicalUri)
|
|
96
|
+
return false
|
|
97
|
+
|
|
98
|
+
resolvedUri.value = canonicalUri
|
|
99
|
+
resolvedPostUrl.value = info.postUrl
|
|
100
|
+
return true
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function create(retries = 10) {
|
|
108
|
+
loaded.value = false
|
|
109
|
+
postNotFound.value = false
|
|
110
|
+
|
|
111
|
+
if (!resolvedUri.value) {
|
|
112
|
+
const ok = await resolvePostUri(props.uri)
|
|
113
|
+
if (!ok) {
|
|
114
|
+
postNotFound.value = true
|
|
115
|
+
loaded.value = true
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await nextTick()
|
|
121
|
+
|
|
122
|
+
// @ts-expect-error global
|
|
123
|
+
if (!window.bluesky?.scan) {
|
|
124
|
+
if (retries <= 0) {
|
|
125
|
+
console.error('Failed to load Bluesky embed widget after 10 retries.')
|
|
126
|
+
postNotFound.value = true
|
|
127
|
+
loaded.value = true
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
setTimeout(create, 1000, retries - 1)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
scan()
|
|
135
|
+
loaded.value = true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onMounted(async () => {
|
|
139
|
+
await create()
|
|
140
|
+
})
|
|
141
|
+
</script>
|
|
142
|
+
|
|
143
|
+
<template>
|
|
144
|
+
<Transform :scale="scale || 1">
|
|
145
|
+
<div ref="container" class="slidev-bluesky">
|
|
146
|
+
<blockquote
|
|
147
|
+
v-if="resolvedUri"
|
|
148
|
+
class="bluesky-embed"
|
|
149
|
+
:data-bluesky-uri="resolvedUri"
|
|
150
|
+
:data-bluesky-embed-color-mode="isDark ? 'dark' : 'light'"
|
|
151
|
+
/>
|
|
152
|
+
<div v-if="!loaded || postNotFound" class="h-30 w-30 my-10px bg-gray-400 bg-opacity-10 rounded-lg flex opacity-50">
|
|
153
|
+
<div class="m-auto animate-pulse text-4xl">
|
|
154
|
+
<div class="i-simple-icons:bluesky" />
|
|
155
|
+
<span v-if="postNotFound">Could not load Bluesky post</span>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</Transform>
|
|
160
|
+
</template>
|
|
161
|
+
|
|
162
|
+
<style>
|
|
163
|
+
.slidev-bluesky .bluesky-embed iframe {
|
|
164
|
+
border-radius: 12px;
|
|
165
|
+
overflow: hidden;
|
|
166
|
+
}
|
|
167
|
+
</style>
|
package/builtin/Link.vue
CHANGED
|
@@ -19,8 +19,8 @@ const { isPrintMode } = useNav()
|
|
|
19
19
|
</script>
|
|
20
20
|
|
|
21
21
|
<template>
|
|
22
|
-
<RouterLink v-if="!isPrintMode && title" :to="String(to)" @click="$event.target
|
|
23
|
-
<RouterLink v-else-if="!isPrintMode && !title" :to="String(to)" @click="$event.target
|
|
22
|
+
<RouterLink v-if="!isPrintMode && title" :to="String(to)" @click="($event.target as HTMLElement)?.blur()" v-html="title" />
|
|
23
|
+
<RouterLink v-else-if="!isPrintMode && !title" :to="String(to)" @click="($event.target as HTMLElement)?.blur()">
|
|
24
24
|
<slot />
|
|
25
25
|
</RouterLink>
|
|
26
26
|
<a v-else-if="isPrintMode && title" :href="`#${to}`" v-html="title" />
|
package/builtin/Monaco.vue
CHANGED
|
@@ -72,7 +72,6 @@ const extMap: Record<string, string> = {
|
|
|
72
72
|
}
|
|
73
73
|
const ext = extMap[props.lang] ?? props.lang
|
|
74
74
|
|
|
75
|
-
const outer = ref<HTMLDivElement>()
|
|
76
75
|
const container = ref<HTMLDivElement>()
|
|
77
76
|
|
|
78
77
|
const contentHeight = ref(0)
|
|
@@ -206,7 +205,7 @@ onMounted(async () => {
|
|
|
206
205
|
|
|
207
206
|
<template>
|
|
208
207
|
<div class="relative slidev-monaco-container">
|
|
209
|
-
<div
|
|
208
|
+
<div class="relative slidev-monaco-container-inner" :style="{ height }">
|
|
210
209
|
<div ref="container" class="absolute inset-0.5" />
|
|
211
210
|
</div>
|
|
212
211
|
<CodeRunner
|
package/builtin/Tweet.vue
CHANGED
|
@@ -27,7 +27,7 @@ async function create(retries = 10) {
|
|
|
27
27
|
if (!window.twttr?.widgets?.createTweet) {
|
|
28
28
|
if (retries <= 0)
|
|
29
29
|
return console.error('Failed to load Twitter widget after 10 retries.')
|
|
30
|
-
setTimeout(
|
|
30
|
+
setTimeout(create, 1000, retries - 1)
|
|
31
31
|
return
|
|
32
32
|
}
|
|
33
33
|
// @ts-expect-error global
|
package/builtin/VClick.ts
CHANGED
|
@@ -19,10 +19,17 @@ export default defineComponent({
|
|
|
19
19
|
type: Boolean,
|
|
20
20
|
default: false,
|
|
21
21
|
},
|
|
22
|
+
/**
|
|
23
|
+
* @deprecated use `animation` instead
|
|
24
|
+
*/
|
|
22
25
|
fade: {
|
|
23
26
|
type: Boolean,
|
|
24
27
|
default: false,
|
|
25
28
|
},
|
|
29
|
+
animation: {
|
|
30
|
+
type: String,
|
|
31
|
+
default: undefined,
|
|
32
|
+
},
|
|
26
33
|
wrapText: {
|
|
27
34
|
type: Function as PropType<(text: VNode) => VNode>,
|
|
28
35
|
default: (text: VNode) => h('span', text),
|
|
@@ -36,6 +43,7 @@ export default defineComponent({
|
|
|
36
43
|
at: this.at,
|
|
37
44
|
hide: this.hide,
|
|
38
45
|
fade: this.fade,
|
|
46
|
+
animation: this.animation,
|
|
39
47
|
handleSpecialElements: false,
|
|
40
48
|
},
|
|
41
49
|
{
|
package/builtin/VClicks.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { normalizeSingleAtValue } from '../composables/useClicks'
|
|
|
11
11
|
import VClickGap from './VClickGap.vue'
|
|
12
12
|
|
|
13
13
|
const listTags = ['ul', 'ol']
|
|
14
|
+
const RE_WHITESPACE_OR_COMMA = /[\s,]+/
|
|
14
15
|
|
|
15
16
|
export default defineComponent({
|
|
16
17
|
props: {
|
|
@@ -30,10 +31,17 @@ export default defineComponent({
|
|
|
30
31
|
type: Boolean,
|
|
31
32
|
default: false,
|
|
32
33
|
},
|
|
34
|
+
/**
|
|
35
|
+
* @deprecated use `animation` instead
|
|
36
|
+
*/
|
|
33
37
|
fade: {
|
|
34
38
|
type: Boolean,
|
|
35
39
|
default: false,
|
|
36
40
|
},
|
|
41
|
+
animation: {
|
|
42
|
+
type: [String, Array<string>],
|
|
43
|
+
default: undefined,
|
|
44
|
+
},
|
|
37
45
|
handleSpecialElements: {
|
|
38
46
|
type: Boolean,
|
|
39
47
|
default: true,
|
|
@@ -53,14 +61,26 @@ export default defineComponent({
|
|
|
53
61
|
const click = resolveDirective('click')!
|
|
54
62
|
|
|
55
63
|
const applyDirective = (node: VNode, value: number | string) => {
|
|
64
|
+
const modifiers: Record<string, boolean> = {
|
|
65
|
+
hide: this.hide,
|
|
66
|
+
}
|
|
67
|
+
if (typeof this.animation === 'string') {
|
|
68
|
+
this.animation.split(RE_WHITESPACE_OR_COMMA).forEach((a) => {
|
|
69
|
+
if (a)
|
|
70
|
+
modifiers[a] = true
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
else if (Array.isArray(this.animation)) {
|
|
74
|
+
this.animation.forEach(a => modifiers[a] = true)
|
|
75
|
+
}
|
|
76
|
+
if (this.fade)
|
|
77
|
+
modifiers.fade = true
|
|
78
|
+
|
|
56
79
|
return withDirectives(node, [[
|
|
57
80
|
click,
|
|
58
81
|
value,
|
|
59
82
|
'',
|
|
60
|
-
|
|
61
|
-
hide: this.hide,
|
|
62
|
-
fade: this.fade,
|
|
63
|
-
},
|
|
83
|
+
modifiers,
|
|
64
84
|
]])
|
|
65
85
|
}
|
|
66
86
|
|
package/builtin/VDrag.vue
CHANGED
|
@@ -8,6 +8,7 @@ const props = defineProps<{
|
|
|
8
8
|
markdownSource?: DragElementMarkdownSource
|
|
9
9
|
}>()
|
|
10
10
|
|
|
11
|
+
// @ts-expect-error TS6133 container is used as template ref
|
|
11
12
|
const { dragId, container, containerStyle, mounted, unmounted, startDragging } = useDragElement(null, props.pos, props.markdownSource)
|
|
12
13
|
|
|
13
14
|
onMounted(mounted)
|
|
@@ -11,6 +11,9 @@ import { useNav } from './useNav'
|
|
|
11
11
|
import { useSlideBounds } from './useSlideBounds'
|
|
12
12
|
import { useDynamicSlideInfo } from './useSlideInfo'
|
|
13
13
|
|
|
14
|
+
const RE_NEWLINE = /\r?\n/g
|
|
15
|
+
const RE_POS_ATTR = /pos=".*?"/
|
|
16
|
+
|
|
14
17
|
export type DragElementDataSource = 'frontmatter' | 'prop' | 'directive'
|
|
15
18
|
/**
|
|
16
19
|
* Markdown source position, injected by markdown-it plugin
|
|
@@ -63,7 +66,7 @@ export function useDragElementsUpdater(no: number) {
|
|
|
63
66
|
throw new Error(`[Slidev] VDrag Element ${id} is missing markdown source`)
|
|
64
67
|
|
|
65
68
|
const [startLine, endLine, idx] = markdownSource
|
|
66
|
-
const lines = info.value.content.split(
|
|
69
|
+
const lines = info.value.content.split(RE_NEWLINE)
|
|
67
70
|
|
|
68
71
|
let section = lines.slice(startLine, endLine).join('\n')
|
|
69
72
|
let replaced = false
|
|
@@ -73,7 +76,7 @@ export function useDragElementsUpdater(no: number) {
|
|
|
73
76
|
? section.replace(/<(v-?drag-?\w*)(.*?)(\/)?>/gi, (full, tag, attrs, selfClose = '', index) => {
|
|
74
77
|
if (index === idx) {
|
|
75
78
|
replaced = true
|
|
76
|
-
const posMatch = attrs.match(
|
|
79
|
+
const posMatch = attrs.match(RE_POS_ATTR)
|
|
77
80
|
if (!posMatch)
|
|
78
81
|
return `<${tag}${ensureSuffix(' ', attrs)}pos="${posStr}"${selfClose}>`
|
|
79
82
|
const start = posMatch.index
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Brush, Options as DrauuOptions, DrawingMode } from 'drauu'
|
|
2
|
-
import { createSharedComposable,
|
|
2
|
+
import { createSharedComposable, useLocalStorage } from '@vueuse/core'
|
|
3
3
|
import { createDrauu } from 'drauu'
|
|
4
4
|
import { computed, markRaw, nextTick, reactive, ref, watch } from 'vue'
|
|
5
5
|
import { configs } from '../env'
|
|
@@ -22,11 +22,11 @@ export const useDrawings = createSharedComposable(() => {
|
|
|
22
22
|
|
|
23
23
|
const drawingEnabled = useLocalStorage('slidev-drawing-enabled', false)
|
|
24
24
|
const drawingPinned = useLocalStorage('slidev-drawing-pinned', false)
|
|
25
|
-
const brush =
|
|
25
|
+
const brush = useLocalStorage<Brush>('slidev-drawing-brush', {
|
|
26
26
|
color: brushColors[0],
|
|
27
27
|
size: 4,
|
|
28
28
|
mode: 'stylus',
|
|
29
|
-
})
|
|
29
|
+
})
|
|
30
30
|
|
|
31
31
|
const isDrawing = ref(false)
|
|
32
32
|
const canUndo = ref(false)
|
|
@@ -46,12 +46,12 @@ export const useDrawings = createSharedComposable(() => {
|
|
|
46
46
|
if (v === 'arrow') {
|
|
47
47
|
// eslint-disable-next-line ts/no-use-before-define
|
|
48
48
|
drauu.mode = 'line'
|
|
49
|
-
brush.arrowEnd = true
|
|
49
|
+
brush.value.arrowEnd = true
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
52
|
// eslint-disable-next-line ts/no-use-before-define
|
|
53
53
|
drauu.mode = v
|
|
54
|
-
brush.arrowEnd = false
|
|
54
|
+
brush.value.arrowEnd = false
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
57
|
})
|
|
@@ -150,7 +150,7 @@ export const useDrawings = createSharedComposable(() => {
|
|
|
150
150
|
clearDrauu()
|
|
151
151
|
}
|
|
152
152
|
else if (e.code.startsWith('Digit') && noModifier && +e.code[5] <= brushColors.length) {
|
|
153
|
-
brush.color = brushColors[+e.code[5] - 1]
|
|
153
|
+
brush.value.color = brushColors[+e.code[5] - 1]
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
156
|
handled = false
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMouse } from '@vueuse/core'
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { mainSlideElement } from '../internals/SlideContainer.vue'
|
|
4
|
+
|
|
5
|
+
export function useMousePosInSlide() {
|
|
6
|
+
const mouse = useMouse()
|
|
7
|
+
|
|
8
|
+
return computed(() => {
|
|
9
|
+
const rect = mainSlideElement.value?.getBoundingClientRect()
|
|
10
|
+
if (!rect)
|
|
11
|
+
return undefined
|
|
12
|
+
|
|
13
|
+
const x = (mouse.x.value - rect.left) / rect.width * 100
|
|
14
|
+
const y = (mouse.y.value - rect.top) / rect.height * 100
|
|
15
|
+
|
|
16
|
+
if (x < 0 || x > 100 || y < 0 || y > 100)
|
|
17
|
+
return undefined
|
|
18
|
+
|
|
19
|
+
return { x, y }
|
|
20
|
+
})
|
|
21
|
+
}
|
|
@@ -5,11 +5,12 @@ import { watchEffect } from 'vue'
|
|
|
5
5
|
|
|
6
6
|
const loaded = new Set<string>()
|
|
7
7
|
const loading = new Set<string>()
|
|
8
|
+
const RE_TRAILING_SLASH = /\/$/
|
|
8
9
|
|
|
9
10
|
function resolveUrl(url: string): string {
|
|
10
11
|
if (url.startsWith('http') || url.startsWith('//'))
|
|
11
12
|
return url
|
|
12
|
-
const base = (import.meta.env.BASE_URL || '/').replace(
|
|
13
|
+
const base = (import.meta.env.BASE_URL || '/').replace(RE_TRAILING_SLASH, '')
|
|
13
14
|
return `${base}${url.startsWith('/') ? url : `/${url}`}`
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { SlideInfo, SlidePatch } from '@slidev/types'
|
|
2
|
-
import type { MaybeRef } from '
|
|
3
|
-
import type { Ref } from 'vue'
|
|
2
|
+
import type { MaybeRef, Ref } from 'vue'
|
|
4
3
|
import { useFetch } from '@vueuse/core'
|
|
5
4
|
import { computed, ref, unref } from 'vue'
|
|
6
5
|
import { getSlide } from '../logic/slides'
|
package/constants.ts
CHANGED
|
@@ -16,12 +16,12 @@ export const injectionSlideZoom = '$$slidev-slide-zoom' as unknown as InjectionK
|
|
|
16
16
|
|
|
17
17
|
export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
|
|
18
18
|
export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
|
|
19
|
-
export const CLASS_VCLICK_FADE = 'slidev-vclick-fade'
|
|
20
19
|
export const CLASS_VCLICK_GONE = 'slidev-vclick-gone'
|
|
21
20
|
export const CLASS_VCLICK_HIDDEN_EXP = 'slidev-vclick-hidden-explicitly'
|
|
22
21
|
export const CLASS_VCLICK_CURRENT = 'slidev-vclick-current'
|
|
23
22
|
export const CLASS_VCLICK_PRIOR = 'slidev-vclick-prior'
|
|
24
23
|
export const CLASS_VCLICK_DISPLAY_NONE = 'slidev-vclick-display-none'
|
|
24
|
+
export const CLASS_VCLICK_ANIMATION_PREFIX = 'slidev-vclick-anim-'
|
|
25
25
|
|
|
26
26
|
export const CLICKS_MAX = 999999
|
|
27
27
|
|
|
@@ -46,6 +46,7 @@ export const FRONTMATTER_FIELDS = [
|
|
|
46
46
|
'zoom',
|
|
47
47
|
'dragPos',
|
|
48
48
|
'lang',
|
|
49
|
+
'clickAnimation',
|
|
49
50
|
]
|
|
50
51
|
|
|
51
52
|
export const HEADMATTER_FIELDS = [
|
|
@@ -32,8 +32,8 @@ function setDrawingMode(mode: typeof drawingMode.value) {
|
|
|
32
32
|
if (mode !== 'eraseLine')
|
|
33
33
|
lastDrawingMode = mode
|
|
34
34
|
}
|
|
35
|
-
function setBrushColor(color:
|
|
36
|
-
brush.color = color
|
|
35
|
+
function setBrushColor(color: string) {
|
|
36
|
+
brush.value.color = color
|
|
37
37
|
drawingEnabled.value = true
|
|
38
38
|
drawingMode.value = lastDrawingMode
|
|
39
39
|
}
|
package/internals/IconButton.vue
CHANGED
|
@@ -6,13 +6,15 @@ const props = defineProps<{
|
|
|
6
6
|
icon?: string
|
|
7
7
|
as?: string
|
|
8
8
|
to?: string
|
|
9
|
+
disabled?: boolean
|
|
10
|
+
active?: boolean
|
|
9
11
|
}>()
|
|
10
12
|
|
|
11
13
|
const type = computed(() => props.as || (props.to ? 'router-link' : 'button'))
|
|
12
14
|
</script>
|
|
13
15
|
|
|
14
16
|
<template>
|
|
15
|
-
<component :is="type" class="slidev-icon-btn" :title="title" :to="to">
|
|
17
|
+
<component :is="type" class="slidev-icon-btn" :class="{ disabled, active }" :title="title" :to="to">
|
|
16
18
|
<span class="sr-only">{{ title }}</span>
|
|
17
19
|
<slot>
|
|
18
20
|
<div :class="icon" />
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onMounted, watchEffect } from 'vue'
|
|
3
|
+
import { useMousePosInSlide } from '../composables/useMousePosInSlide'
|
|
4
|
+
import { useNav } from '../composables/useNav'
|
|
5
|
+
import { cursorStyle } from '../state'
|
|
6
|
+
import { sharedState } from '../state/shared'
|
|
7
|
+
|
|
8
|
+
const selfMouse = useMousePosInSlide()
|
|
9
|
+
const { isPresenter } = useNav()
|
|
10
|
+
|
|
11
|
+
const laserPointer = computed(() => {
|
|
12
|
+
if (!isPresenter.value && sharedState.cursor?.style === 'laser') {
|
|
13
|
+
return sharedState.cursor
|
|
14
|
+
}
|
|
15
|
+
if (cursorStyle.value === 'laser') {
|
|
16
|
+
return selfMouse.value
|
|
17
|
+
}
|
|
18
|
+
return null
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
onMounted(() => {
|
|
22
|
+
watchEffect(() => {
|
|
23
|
+
document.body.classList.toggle('slidev-self-laser-active', cursorStyle.value === 'laser' && !!laserPointer.value)
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<template>
|
|
29
|
+
<div
|
|
30
|
+
v-if="laserPointer"
|
|
31
|
+
class="absolute top-0 left-0 right-0 bottom-0 pointer-events-none text-xl"
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
class="laser-pointer"
|
|
35
|
+
:style="{
|
|
36
|
+
left: `${laserPointer.x}%`,
|
|
37
|
+
top: `${laserPointer.y}%`,
|
|
38
|
+
}"
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<style scoped>
|
|
44
|
+
.laser-pointer {
|
|
45
|
+
position: absolute;
|
|
46
|
+
width: 6px;
|
|
47
|
+
height: 6px;
|
|
48
|
+
border-radius: 9999px;
|
|
49
|
+
transform: translate(-50%, -50%);
|
|
50
|
+
background: radial-gradient(
|
|
51
|
+
circle at center,
|
|
52
|
+
rgba(255, 80, 80, 1) 0%,
|
|
53
|
+
rgba(255, 0, 0, 0.95) 55%,
|
|
54
|
+
rgba(255, 0, 0, 0.1) 100%
|
|
55
|
+
);
|
|
56
|
+
box-shadow:
|
|
57
|
+
0 0 10px rgba(255, 0, 0, 0.9),
|
|
58
|
+
0 0 22px rgba(255, 0, 0, 0.55);
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.slidev-self-laser-active .slidev-slide-container {
|
|
64
|
+
cursor: none;
|
|
65
|
+
}
|
|
66
|
+
</style>
|
package/internals/MenuButton.vue
CHANGED
|
@@ -25,7 +25,7 @@ onClickOutside(el, () => {
|
|
|
25
25
|
<template>
|
|
26
26
|
<div ref="el" class="flex relative">
|
|
27
27
|
<button :class="{ disabled }" @click="value = !value">
|
|
28
|
-
<slot name="button" :class="{ disabled }" />
|
|
28
|
+
<slot name="button" :value :class="{ disabled }" />
|
|
29
29
|
</button>
|
|
30
30
|
<KeepAlive>
|
|
31
31
|
<div
|
|
@@ -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>
|