@slidev/client 0.48.9 → 0.49.0-beta.2

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.
@@ -0,0 +1,286 @@
1
+ import { debounce, ensureSuffix } from '@antfu/utils'
2
+ import type { SlidePatch } from '@slidev/types'
3
+ import { injectLocal, onClickOutside, useWindowFocus } from '@vueuse/core'
4
+ import type { CSSProperties, DirectiveBinding, InjectionKey, WatchStopHandle } from 'vue'
5
+ import { computed, ref, watch } from 'vue'
6
+ import { injectionCurrentPage, injectionFrontmatter, injectionRenderContext, injectionSlideElement, injectionSlideScale, injectionSlideZoom } from '../constants'
7
+ import { makeId } from '../logic/utils'
8
+ import { activeDragElement } from '../state'
9
+ import { directiveInject } from '../utils'
10
+ import { useSlideBounds } from './useSlideBounds'
11
+ import { useDynamicSlideInfo } from './useSlideInfo'
12
+
13
+ export type DragElementDataSource = 'frontmatter' | 'prop' | 'directive'
14
+ /**
15
+ * Markdown source position, injected by markdown-it plugin
16
+ */
17
+ export type DragElementMarkdownSource = [startLine: number, endLine: number, index: number]
18
+
19
+ export type DragElementsUpdater = (id: string, posStr: string, type: DragElementDataSource, markdownSource?: DragElementMarkdownSource) => void
20
+
21
+ const map: Record<number, DragElementsUpdater> = {}
22
+
23
+ export function useDragElementsUpdater(no: number) {
24
+ if (!(__DEV__ && __SLIDEV_FEATURE_EDITOR__))
25
+ return () => {}
26
+
27
+ if (map[no])
28
+ return map[no]
29
+
30
+ const { info, update } = useDynamicSlideInfo(no)
31
+
32
+ let newPatch: SlidePatch | null = null
33
+ async function save() {
34
+ if (newPatch) {
35
+ await update({
36
+ ...newPatch,
37
+ skipHmr: true,
38
+ })
39
+ newPatch = null
40
+ }
41
+ }
42
+ const debouncedSave = debounce(500, save)
43
+
44
+ return map[no] = (id, posStr, type, markdownSource) => {
45
+ if (!info.value)
46
+ return
47
+
48
+ if (type === 'frontmatter') {
49
+ const frontmatter = info.value.frontmatter
50
+ frontmatter.dragPos ||= {}
51
+ if (frontmatter.dragPos[id] === posStr)
52
+ return
53
+ frontmatter.dragPos[id] = posStr
54
+ newPatch = {
55
+ frontmatter: {
56
+ dragPos: frontmatter.dragPos,
57
+ },
58
+ }
59
+ }
60
+ else {
61
+ if (!markdownSource)
62
+ throw new Error(`[Slidev] VDrag Element ${id} is missing markdown source`)
63
+
64
+ const [startLine, endLine, idx] = markdownSource
65
+ const lines = info.value.content.split(/\r?\n/g)
66
+
67
+ let section = lines.slice(startLine, endLine).join('\n')
68
+ let replaced = false
69
+
70
+ section = type === 'prop'
71
+ ? section.replace(/<(v-?drag)(.*?)>/ig, (full, tag, attrs, index) => {
72
+ if (index === idx) {
73
+ replaced = true
74
+ const posMatch = attrs.match(/pos=".*?"/)
75
+ if (!posMatch)
76
+ return `<${tag}${ensureSuffix(' ', attrs)}pos="${posStr}">`
77
+ const start = posMatch.index
78
+ const end = start + posMatch[0].length
79
+ return `<${tag}${attrs.slice(0, start)}pos="${posStr}"${attrs.slice(end)}>`
80
+ }
81
+ return full
82
+ })
83
+ : section.replace(/(?<![</\w])v-drag(?:=".*?")?/ig, (full, index) => {
84
+ if (index === idx) {
85
+ replaced = true
86
+ return `v-drag="${posStr}"`
87
+ }
88
+ return full
89
+ })
90
+
91
+ if (!replaced)
92
+ throw new Error(`[Slidev] VDrag Element ${id} is not found in the markdown source`)
93
+
94
+ lines.splice(
95
+ startLine,
96
+ endLine - startLine,
97
+ section,
98
+ )
99
+
100
+ const newContent = lines.join('\n')
101
+ if (info.value.content === newContent)
102
+ return
103
+ newPatch = {
104
+ content: newContent,
105
+ }
106
+ info.value = {
107
+ ...info.value,
108
+ content: newContent,
109
+ }
110
+ }
111
+ debouncedSave()
112
+ }
113
+ }
114
+
115
+ export function useDragElement(directive: DirectiveBinding | null, posRaw?: string | number | number[], markdownSource?: DragElementMarkdownSource) {
116
+ function inject<T>(key: InjectionKey<T> | string): T | undefined {
117
+ return directive
118
+ ? directiveInject(directive, key)
119
+ : injectLocal(key)
120
+ }
121
+
122
+ const renderContext = inject(injectionRenderContext)!
123
+ const frontmatter = inject(injectionFrontmatter) ?? {}
124
+ const page = inject(injectionCurrentPage)!
125
+ const updater = computed(() => useDragElementsUpdater(page.value))
126
+ const scale = inject(injectionSlideScale) ?? ref(1)
127
+ const zoom = inject(injectionSlideZoom) ?? ref(1)
128
+ const { left: slideLeft, top: slideTop, stop: stopWatchBounds } = useSlideBounds(inject(injectionSlideElement) ?? ref())
129
+ const enabled = ['slide', 'presenter'].includes(renderContext.value)
130
+
131
+ let dataSource: DragElementDataSource = directive ? 'directive' : 'prop'
132
+ let id: string = makeId()
133
+ let pos: number[] | undefined
134
+ if (Array.isArray(posRaw)) {
135
+ pos = posRaw
136
+ }
137
+ else if (typeof posRaw === 'string' && posRaw.includes(',')) {
138
+ pos = posRaw.split(',').map(Number)
139
+ }
140
+ else if (posRaw != null) {
141
+ dataSource = 'frontmatter'
142
+ id = `${posRaw}`
143
+ posRaw = frontmatter?.dragPos?.[id]
144
+ pos = (posRaw as string)?.split(',').map(Number)
145
+ }
146
+
147
+ if (dataSource !== 'frontmatter' && !markdownSource)
148
+ throw new Error('[Slidev] Can not identify the source position of the v-drag element, please provide an explicit `id` prop.')
149
+
150
+ const watchStopHandles: WatchStopHandle[] = [stopWatchBounds]
151
+
152
+ const autoHeight = posRaw != null && !Number.isFinite(pos?.[3])
153
+ pos ??= [Number.NaN, Number.NaN, 0]
154
+ const width = ref(pos[2])
155
+ const x0 = ref(pos[0] + pos[2] / 2)
156
+
157
+ const rotate = ref(pos[4] ?? 0)
158
+ const rotateRad = computed(() => rotate.value * Math.PI / 180)
159
+ const rotateSin = computed(() => Math.sin(rotateRad.value))
160
+ const rotateCos = computed(() => Math.cos(rotateRad.value))
161
+
162
+ const container = ref<HTMLElement>()
163
+ const bounds = ref({ left: 0, top: 0, width: 0, height: 0 })
164
+ const actualHeight = ref(0)
165
+ function updateBounds() {
166
+ const rect = container.value!.getBoundingClientRect()
167
+ bounds.value = {
168
+ left: rect.left / zoom.value,
169
+ top: rect.top / zoom.value,
170
+ width: rect.width / zoom.value,
171
+ height: rect.height / zoom.value,
172
+ }
173
+ actualHeight.value = ((bounds.value.width + bounds.value.height) / scale.value / (Math.abs(rotateSin.value) + Math.abs(rotateCos.value)) - width.value)
174
+ }
175
+ watchStopHandles.push(watch(width, updateBounds, { flush: 'post' }))
176
+
177
+ const configuredHeight = ref(pos[3] ?? 0)
178
+ const height = computed({
179
+ get: () => (autoHeight ? actualHeight.value : configuredHeight.value) || 0,
180
+ set: v => !autoHeight && (configuredHeight.value = v),
181
+ })
182
+ const configuredY0 = ref(pos[1])
183
+ const y0 = computed({
184
+ get: () => configuredY0.value + height.value / 2,
185
+ set: v => configuredY0.value = v - height.value / 2,
186
+ })
187
+
188
+ const containerStyle = computed<CSSProperties>(() => {
189
+ return Number.isFinite(x0.value)
190
+ ? {
191
+ position: 'absolute',
192
+ zIndex: 100,
193
+ left: `${x0.value - width.value / 2}px`,
194
+ top: `${y0.value - height.value / 2}px`,
195
+ width: `${width.value}px`,
196
+ height: autoHeight ? undefined : `${height.value}px`,
197
+ transformOrigin: 'center center',
198
+ transform: `rotate(${rotate.value}deg)`,
199
+ }
200
+ : {
201
+ position: 'absolute',
202
+ zIndex: 100,
203
+ }
204
+ })
205
+
206
+ watchStopHandles.push(
207
+ watch(
208
+ [x0, y0, width, height, rotate],
209
+ ([x0, y0, w, h, r]) => {
210
+ let posStr = [x0 - w / 2, y0 - h / 2, w].map(Math.round).join()
211
+ if (autoHeight)
212
+ posStr += dataSource === 'directive' ? ',NaN' : ',_'
213
+ else
214
+ posStr += `,${Math.round(h)}`
215
+ if (Math.round(r) !== 0)
216
+ posStr += `,${Math.round(r)}`
217
+
218
+ if (dataSource === 'directive')
219
+ posStr = `[${posStr}]`
220
+
221
+ updater.value(id, posStr, dataSource, markdownSource)
222
+ },
223
+ ),
224
+ )
225
+
226
+ const state = {
227
+ id,
228
+ dataSource,
229
+ markdownSource,
230
+ zoom,
231
+ autoHeight,
232
+ x0,
233
+ y0,
234
+ width,
235
+ height,
236
+ rotate,
237
+ container,
238
+ containerStyle,
239
+ watchStopHandles,
240
+ dragging: computed((): boolean => activeDragElement.value === state),
241
+ mounted() {
242
+ if (!enabled)
243
+ return
244
+ updateBounds()
245
+ if (!posRaw) {
246
+ setTimeout(() => {
247
+ updateBounds()
248
+ x0.value = (bounds.value.left + bounds.value.width / 2 - slideLeft.value) / scale.value
249
+ y0.value = (bounds.value.top - slideTop.value) / scale.value
250
+ width.value = bounds.value.width / scale.value
251
+ height.value = bounds.value.height / scale.value
252
+ }, 100)
253
+ }
254
+ },
255
+ unmounted() {
256
+ if (!enabled)
257
+ return
258
+ state.stopDragging()
259
+ },
260
+ startDragging(): void {
261
+ updateBounds()
262
+ activeDragElement.value = state
263
+ },
264
+ stopDragging(): void {
265
+ if (activeDragElement.value === state)
266
+ activeDragElement.value = null
267
+ },
268
+ }
269
+
270
+ watchStopHandles.push(
271
+ onClickOutside(container, (ev) => {
272
+ const container = document.querySelector('#drag-control-container')
273
+ if (container && ev.target && container.contains(ev.target as HTMLElement))
274
+ return
275
+ state.stopDragging()
276
+ }),
277
+ watch(useWindowFocus(), (focused) => {
278
+ if (!focused)
279
+ state.stopDragging()
280
+ }),
281
+ )
282
+
283
+ return state
284
+ }
285
+
286
+ export type DragElementState = ReturnType<typeof useDragElement>
@@ -60,6 +60,11 @@ export interface SlidevContextNav {
60
60
  goFirst: () => Promise<void>
61
61
  /** Go to the last slide */
62
62
  goLast: () => Promise<void>
63
+
64
+ /** Enter presenter mode */
65
+ enterPresenter: () => void
66
+ /** Exit presenter mode */
67
+ exitPresenter: () => void
63
68
  }
64
69
 
65
70
  export interface SlidevContextNavState {
@@ -194,6 +199,19 @@ export function useNavBase(
194
199
  }
195
200
  }
196
201
 
202
+ function enterPresenter() {
203
+ router?.push({
204
+ path: getSlidePath(currentSlideNo.value, true),
205
+ query: { ...router.currentRoute.value.query },
206
+ })
207
+ }
208
+ function exitPresenter() {
209
+ router?.push({
210
+ path: getSlidePath(currentSlideNo.value, false),
211
+ query: { ...router.currentRoute.value.query },
212
+ })
213
+ }
214
+
197
215
  return {
198
216
  slides,
199
217
  total,
@@ -222,6 +240,8 @@ export function useNavBase(
222
240
  goFirst,
223
241
  nextSlide,
224
242
  prevSlide,
243
+ enterPresenter,
244
+ exitPresenter,
225
245
  }
226
246
  }
227
247
 
@@ -0,0 +1,30 @@
1
+ import { useElementBounding } from '@vueuse/core'
2
+ import { inject, ref, watch } from 'vue'
3
+ import { injectionSlideElement } from '../constants'
4
+ import { editorHeight, editorWidth, isEditorVertical, showEditor, slideScale, windowSize } from '../state'
5
+
6
+ export function useSlideBounds(slideElement = inject(injectionSlideElement, ref())) {
7
+ const bounding = useElementBounding(slideElement)
8
+ const stop = watch(
9
+ [
10
+ showEditor,
11
+ isEditorVertical,
12
+ editorWidth,
13
+ editorHeight,
14
+ slideScale,
15
+ windowSize.width,
16
+ windowSize.height,
17
+ ],
18
+ () => {
19
+ setTimeout(bounding.update, 300)
20
+ },
21
+ {
22
+ flush: 'post',
23
+ immediate: true,
24
+ },
25
+ )
26
+ return {
27
+ ...bounding,
28
+ stop,
29
+ }
30
+ }
@@ -6,19 +6,19 @@ import type { SlideInfo, SlidePatch } from '@slidev/types'
6
6
  import { getSlide } from '../logic/slides'
7
7
 
8
8
  export interface UseSlideInfo {
9
- info: Ref<SlideInfo | undefined>
9
+ info: Ref<SlideInfo | null>
10
10
  update: (data: SlidePatch) => Promise<SlideInfo | void>
11
11
  }
12
12
 
13
13
  export function useSlideInfo(no: number): UseSlideInfo {
14
14
  if (!__SLIDEV_HAS_SERVER__) {
15
15
  return {
16
- info: ref(getSlide(no)?.meta.slide) as Ref<SlideInfo | undefined>,
16
+ info: ref(getSlide(no)?.meta.slide ?? null) as Ref<SlideInfo | null>,
17
17
  update: async () => {},
18
18
  }
19
19
  }
20
20
  const url = `/@slidev/slide/${no}.json`
21
- const { data: info, execute } = useFetch(url).json().get()
21
+ const { data: info, execute } = useFetch(url).json<SlideInfo>().get()
22
22
 
23
23
  execute()
24
24
 
@@ -42,7 +42,7 @@ export function useSlideInfo(no: number): UseSlideInfo {
42
42
  info.value = payload.data
43
43
  })
44
44
  import.meta.hot?.on('slidev:update-note', (payload) => {
45
- if (payload.no === no && info.value.note?.trim() !== payload.note?.trim())
45
+ if (payload.no === no && info.value && info.value.note?.trim() !== payload.note?.trim())
46
46
  info.value = { ...info.value, ...payload }
47
47
  })
48
48
  }
@@ -61,7 +61,14 @@ export function useDynamicSlideInfo(no: MaybeRef<number>) {
61
61
  }
62
62
 
63
63
  return {
64
- info: computed(() => get(unref(no)).info.value),
64
+ info: computed({
65
+ get() {
66
+ return get(unref(no)).info.value
67
+ },
68
+ set(newInfo) {
69
+ get(unref(no)).info.value = newInfo
70
+ },
71
+ }),
65
72
  update: async (data: SlidePatch, newId?: number) => {
66
73
  const info = get(newId ?? unref(no))
67
74
  const newData = await info.update(data)
package/constants.ts CHANGED
@@ -6,6 +6,7 @@ import type { SlidevContext } from './modules/context'
6
6
  // The value of the injections keys are implementation details, you should always use them with the reference to the constant instead of the value
7
7
  export const injectionClicksContext = '$$slidev-clicks-context' as unknown as InjectionKey<Ref<ClicksContext>>
8
8
  export const injectionCurrentPage = '$$slidev-page' as unknown as InjectionKey<Ref<number>>
9
+ export const injectionSlideElement = '$$slidev-slide-element' as unknown as InjectionKey<Ref<HTMLElement | null>>
9
10
  export const injectionSlideScale = '$$slidev-slide-scale' as unknown as InjectionKey<ComputedRef<number>>
10
11
  export const injectionSlidevContext = '$$slidev-context' as unknown as InjectionKey<UnwrapNestedRefs<SlidevContext>>
11
12
  export const injectionRoute = '$$slidev-route' as unknown as InjectionKey<SlideRoute>
@@ -13,7 +14,6 @@ export const injectionRenderContext = '$$slidev-render-context' as unknown as In
13
14
  export const injectionActive = '$$slidev-active' as unknown as InjectionKey<Ref<boolean>>
14
15
  export const injectionFrontmatter = '$$slidev-fontmatter' as unknown as InjectionKey<Record<string, any>>
15
16
  export const injectionSlideZoom = '$$slidev-slide-zoom' as unknown as InjectionKey<ComputedRef<number>>
16
- export const injectionClickVisibility = '$$slidev-click-visibility' as unknown as InjectionKey<ComputedRef<true | 'before' | 'after'>>
17
17
 
18
18
  export const CLASS_VCLICK_TARGET = 'slidev-vclick-target'
19
19
  export const CLASS_VCLICK_HIDDEN = 'slidev-vclick-hidden'
@@ -44,6 +44,7 @@ export const FRONTMATTER_FIELDS = [
44
44
  'title',
45
45
  'transition',
46
46
  'zoom',
47
+ 'dragPos',
47
48
  ]
48
49
 
49
50
  export const HEADMATTER_FIELDS = [
@@ -75,4 +76,5 @@ export const HEADMATTER_FIELDS = [
75
76
  'drawings',
76
77
  'htmlAttrs',
77
78
  'mdc',
79
+ 'contextMenu',
78
80
  ]
package/env.ts CHANGED
@@ -4,6 +4,8 @@ import configs from '#slidev/configs'
4
4
 
5
5
  export { configs }
6
6
 
7
+ export const mode = __DEV__ ? 'dev' : 'build'
8
+
7
9
  export const slideAspect = ref(configs.aspectRatio ?? (16 / 9))
8
10
  export const slideWidth = ref(configs.canvasWidth ?? 980)
9
11
 
@@ -1,11 +1,13 @@
1
1
  <script setup lang="ts">
2
2
  import { debounce, toArray } from '@antfu/utils'
3
3
  import { useVModel } from '@vueuse/core'
4
- import type { CodeRunnerOutput } from '@slidev/types'
5
- import { computed, ref, shallowRef, watch } from 'vue'
4
+ import type { CodeRunnerOutput, RawAtValue } from '@slidev/types'
5
+ import { computed, onMounted, onUnmounted, ref, shallowRef, watch, watchSyncEffect } from 'vue'
6
6
  import { useSlideContext } from '../context'
7
7
  import setupCodeRunners from '../setup/code-runners'
8
8
  import { useNav } from '../composables/useNav'
9
+ import { makeId } from '../logic/utils'
10
+ import { normalizeAtValue } from '../composables/useClicks'
9
11
  import IconButton from './IconButton.vue'
10
12
  import DomElement from './DomElement.vue'
11
13
 
@@ -14,6 +16,7 @@ const props = defineProps<{
14
16
  lang: string
15
17
  autorun: boolean | 'once'
16
18
  height?: string
19
+ showOutputAt?: RawAtValue
17
20
  highlightOutput: boolean
18
21
  runnerOptions?: Record<string, unknown>
19
22
  }>()
@@ -24,7 +27,7 @@ const { isPrintMode } = useNav()
24
27
 
25
28
  const code = useVModel(props, 'modelValue', emit)
26
29
 
27
- const { $renderContext } = useSlideContext()
30
+ const { $renderContext, $clicksContext } = useSlideContext()
28
31
  const disabled = computed(() => !['slide', 'presenter'].includes($renderContext.value))
29
32
 
30
33
  const autorun = isPrintMode.value ? 'once' : props.autorun
@@ -33,6 +36,25 @@ const outputs = shallowRef<CodeRunnerOutput[]>()
33
36
  const runCount = ref(0)
34
37
  const highlightFn = ref<(code: string, lang: string) => string>()
35
38
 
39
+ const hidden = ref(props.showOutputAt)
40
+ if (props.showOutputAt) {
41
+ const id = makeId()
42
+ onMounted(() => {
43
+ const at = normalizeAtValue(props.showOutputAt)
44
+ const info = $clicksContext.calculate(at)
45
+ if (info) {
46
+ $clicksContext.register(id, info)
47
+ watchSyncEffect(() => {
48
+ hidden.value = !info.isActive.value
49
+ })
50
+ }
51
+ else {
52
+ hidden.value = false
53
+ }
54
+ })
55
+ onUnmounted(() => $clicksContext.unregister(id))
56
+ }
57
+
36
58
  const triggerRun = debounce(200, async () => {
37
59
  if (disabled.value)
38
60
  return
@@ -59,6 +81,7 @@ else if (autorun)
59
81
 
60
82
  <template>
61
83
  <div
84
+ v-show="!hidden"
62
85
  class="relative flex flex-col rounded-b border-t border-main"
63
86
  :style="{ height: props.height }"
64
87
  data-waitfor=".slidev-runner-output"
@@ -0,0 +1,110 @@
1
+ <script setup lang="ts">
2
+ import { onClickOutside, useElementBounding, useEventListener, useWindowFocus } from '@vueuse/core'
3
+ import { computed, ref, watch } from 'vue'
4
+ import { closeContextMenu, currentContextMenu } from '../logic/contextMenu'
5
+ import { useDynamicSlideInfo } from '../composables/useSlideInfo'
6
+ import { windowSize } from '../state'
7
+ import { configs } from '../env'
8
+
9
+ const container = ref<HTMLElement>()
10
+
11
+ onClickOutside(container, closeContextMenu)
12
+ useEventListener(document, 'mousedown', (ev) => {
13
+ if (ev.buttons & 2)
14
+ closeContextMenu()
15
+ }, {
16
+ passive: true,
17
+ capture: true,
18
+ })
19
+
20
+ const isExplicitEnabled = computed(() => configs.contextMenu != null)
21
+
22
+ const windowFocus = useWindowFocus()
23
+ watch(windowFocus, (hasFocus) => {
24
+ if (!hasFocus)
25
+ closeContextMenu()
26
+ })
27
+
28
+ const firstSlide = useDynamicSlideInfo(1)
29
+ function disableContextMenu() {
30
+ const info = firstSlide.info.value
31
+ if (!info)
32
+ return
33
+ firstSlide.update({
34
+ frontmatter: {
35
+ contextMenu: false,
36
+ },
37
+ })
38
+ }
39
+
40
+ const { width, height } = useElementBounding(container)
41
+ const left = computed(() => {
42
+ const x = currentContextMenu.value?.x
43
+ if (!x)
44
+ return 0
45
+ if (x + width.value > windowSize.width.value)
46
+ return windowSize.width.value - width.value
47
+ return x
48
+ })
49
+ const top = computed(() => {
50
+ const y = currentContextMenu.value?.y
51
+ if (!y)
52
+ return 0
53
+ if (y + height.value > windowSize.height.value)
54
+ return windowSize.height.value - height.value
55
+ return y
56
+ })
57
+ </script>
58
+
59
+ <template>
60
+ <div
61
+ v-if="currentContextMenu"
62
+ ref="container"
63
+ :style="`left:${left}px;top:${top}px`"
64
+ class="fixed z-100 w-60 flex flex-wrap justify-items-start p-1 animate-fade-in animate-duration-100 backdrop-blur bg-main bg-opacity-75! border border-main rounded-md shadow overflow-hidden select-none"
65
+ @contextmenu.prevent=""
66
+ @click="closeContextMenu"
67
+ >
68
+ <template v-for="item, index of currentContextMenu.items.value" :key="index">
69
+ <div v-if="item === 'separator'" :key="index" class="w-full my1 border-t border-main" />
70
+ <div
71
+ v-else-if="item.small"
72
+ class="p-2 w-[40px] h-[40px] inline-block text-center cursor-pointer rounded"
73
+ :class="item.disabled ? `op40` : `hover:bg-active`"
74
+ :title="(item.label as string)"
75
+ @click="item.action"
76
+ >
77
+ <component :is="item.icon" />
78
+ </div>
79
+ <div
80
+ v-else
81
+ class="w-full grid grid-cols-[35px_1fr] p-2 pl-0 cursor-pointer rounded"
82
+ :class="item.disabled ? `op40` : `hover:bg-active`"
83
+ @click="item.action"
84
+ >
85
+ <div class="mx-auto">
86
+ <component :is="item.icon" />
87
+ </div>
88
+ <div v-if="typeof item.label === 'string'">
89
+ {{ item.label }}
90
+ </div>
91
+ <component :is="item.label" v-else />
92
+ </div>
93
+ </template>
94
+ <template v-if="!isExplicitEnabled">
95
+ <div class="w-full my1 border-t border-main" />
96
+ <div class="w-full text-xs p2">
97
+ <div class="text-main text-opacity-50!">
98
+ Hold <kbd class="border px1 py0.5 border-main rounded text-primary">Shift</kbd> and right click to open the native context menu
99
+ <button
100
+ v-if="__DEV__"
101
+ class="underline op50 hover:op100 mt1 block"
102
+ @click="disableContextMenu()"
103
+ >
104
+ Disable custom context menu
105
+ </button>
106
+ </div>
107
+ </div>
108
+ </template>
109
+ </div>
110
+ </template>
@@ -5,6 +5,7 @@ import { configs } from '../env'
5
5
  import QuickOverview from './QuickOverview.vue'
6
6
  import InfoDialog from './InfoDialog.vue'
7
7
  import Goto from './Goto.vue'
8
+ import ContextMenu from './ContextMenu.vue'
8
9
 
9
10
  const WebCamera = shallowRef<any>()
10
11
  const RecordingDialog = shallowRef<any>()
@@ -20,4 +21,5 @@ if (__SLIDEV_FEATURE_RECORD__) {
20
21
  <WebCamera v-if="WebCamera" />
21
22
  <RecordingDialog v-if="RecordingDialog" v-model="showRecordingDialog" />
22
23
  <InfoDialog v-if="configs.info" v-model="showInfoDialog" />
24
+ <ContextMenu />
23
25
  </template>