@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.
Files changed (43) 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 +25 -7
  16. package/internals/IconButton.vue +3 -1
  17. package/internals/LaserPointer.vue +66 -0
  18. package/internals/MenuButton.vue +1 -1
  19. package/internals/NavControls.vue +28 -18
  20. package/internals/PresenterMouse.vue +1 -1
  21. package/internals/Settings.vue +10 -1
  22. package/internals/ShikiEditor.vue +2 -4
  23. package/internals/SideEditor.vue +3 -1
  24. package/internals/SlideContainer.vue +18 -1
  25. package/logic/recording.ts +16 -8
  26. package/logic/slides.ts +20 -7
  27. package/modules/v-click.ts +42 -8
  28. package/package.json +22 -21
  29. package/pages/404.vue +3 -1
  30. package/pages/export.vue +5 -1
  31. package/pages/notes-edit.vue +3 -1
  32. package/pages/play.vue +2 -0
  33. package/pages/presenter.vue +18 -23
  34. package/setup/main.ts +1 -2
  35. package/setup/monaco.ts +4 -4
  36. package/setup/root.ts +1 -1
  37. package/shim-vue.d.ts +3 -1
  38. package/state/shared.ts +1 -0
  39. package/state/storage.ts +7 -0
  40. package/styles/index.css +8 -22
  41. package/styles/index.ts +23 -0
  42. package/styles/layouts-base.css +1 -1
  43. 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>
@@ -48,7 +48,7 @@ const props = defineProps({
48
48
  },
49
49
  title: {
50
50
  type: String,
51
- default: undefined,
51
+ default: '',
52
52
  },
53
53
  })
54
54
 
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.blur()" v-html="title" />
23
- <RouterLink v-else-if="!isPrintMode && !title" :to="String(to)" @click="$event.target.blur()">
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" />
@@ -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 ref="outer" class="relative slidev-monaco-container-inner" :style="{ height }">
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(() => create(retries - 1), 1000)
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
  {
@@ -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(/\r?\n/g)
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(/pos=".*?"/)
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, toReactive, useLocalStorage } from '@vueuse/core'
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 = toReactive(useLocalStorage<Brush>('slidev-drawing-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 '@vueuse/core'
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 { Menu } from 'floating-vue'
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: typeof brush.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
- <Menu>
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
- </Menu>
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-menu .v-popper__arrow-inner {
155
+ .v-popper--theme-dropdown .v-popper__arrow-inner {
138
156
  --uno: border-main;
139
157
  }
140
158
  </style>
@@ -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>
@@ -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