@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
|
@@ -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 = [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { Dropdown } from 'floating-vue'
|
|
3
|
+
import { ref } from 'vue'
|
|
3
4
|
import { useDrawings } from '../composables/useDrawings'
|
|
4
5
|
import Draggable from './Draggable.vue'
|
|
5
6
|
import IconButton from './IconButton.vue'
|
|
@@ -18,6 +19,23 @@ const {
|
|
|
18
19
|
brushColors,
|
|
19
20
|
} = useDrawings()
|
|
20
21
|
|
|
22
|
+
const strokeWidthDropdownShown = ref(false)
|
|
23
|
+
|
|
24
|
+
function showStrokeWidthDropdown(event: Event) {
|
|
25
|
+
event.preventDefault()
|
|
26
|
+
event.stopPropagation()
|
|
27
|
+
requestAnimationFrame(() => {
|
|
28
|
+
requestAnimationFrame(() => {
|
|
29
|
+
strokeWidthDropdownShown.value = true
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function showStrokeWidthDropdownForPointer(event: PointerEvent) {
|
|
35
|
+
if (event.pointerType !== 'mouse')
|
|
36
|
+
showStrokeWidthDropdown(event)
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
function undo() {
|
|
22
40
|
drauu.undo()
|
|
23
41
|
}
|
|
@@ -32,8 +50,8 @@ function setDrawingMode(mode: typeof drawingMode.value) {
|
|
|
32
50
|
if (mode !== 'eraseLine')
|
|
33
51
|
lastDrawingMode = mode
|
|
34
52
|
}
|
|
35
|
-
function setBrushColor(color:
|
|
36
|
-
brush.color = color
|
|
53
|
+
function setBrushColor(color: string) {
|
|
54
|
+
brush.value.color = color
|
|
37
55
|
drawingEnabled.value = true
|
|
38
56
|
drawingMode.value = lastDrawingMode
|
|
39
57
|
}
|
|
@@ -71,8 +89,8 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
71
89
|
|
|
72
90
|
<VerticalDivider />
|
|
73
91
|
|
|
74
|
-
<
|
|
75
|
-
<IconButton title="Adjust stroke width" :class="{ shallow: drawingMode === 'eraseLine' }">
|
|
92
|
+
<Dropdown v-model:shown="strokeWidthDropdownShown" :triggers="[]">
|
|
93
|
+
<IconButton title="Adjust stroke width" :class="{ shallow: drawingMode === 'eraseLine' }" @click="showStrokeWidthDropdown" @pointerdown="showStrokeWidthDropdownForPointer" @pointerup="showStrokeWidthDropdownForPointer" @touchend="showStrokeWidthDropdown">
|
|
76
94
|
<svg viewBox="0 0 32 32" width="1.2em" height="1.2em">
|
|
77
95
|
<line x1="2" y1="15" x2="22" y2="4" stroke="currentColor" stroke-width="1" stroke-linecap="round" />
|
|
78
96
|
<line x1="2" y1="24" x2="28" y2="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
|
|
@@ -89,7 +107,7 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
89
107
|
</div>
|
|
90
108
|
</div>
|
|
91
109
|
</template>
|
|
92
|
-
</
|
|
110
|
+
</Dropdown>
|
|
93
111
|
<IconButton
|
|
94
112
|
v-for="color of brushColors"
|
|
95
113
|
:key="color"
|
|
@@ -134,7 +152,7 @@ function setBrushColor(color: typeof brush.color) {
|
|
|
134
152
|
</template>
|
|
135
153
|
|
|
136
154
|
<style>
|
|
137
|
-
.v-popper--theme-
|
|
155
|
+
.v-popper--theme-dropdown .v-popper__arrow-inner {
|
|
138
156
|
--uno: border-main;
|
|
139
157
|
}
|
|
140
158
|
</style>
|
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
|