@slidev/client 0.48.0-beta.11 → 0.48.0-beta.13
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/ShikiMagicMove.vue +48 -0
- package/builtin/VClick.ts +2 -1
- package/composables/useClicks.ts +22 -14
- package/constants.ts +2 -0
- package/context.ts +5 -2
- package/internals/DrawingLayer.vue +2 -3
- package/internals/NoteDisplay.vue +108 -2
- package/internals/NoteEditor.vue +6 -0
- package/internals/NoteStatic.vue +3 -0
- package/internals/OverviewClicksSlider.vue +91 -0
- package/internals/PrintSlide.vue +8 -2
- package/internals/SlidesOverview.vue +2 -1
- package/logic/nav.ts +2 -1
- package/package.json +5 -4
- package/pages/notes.vue +1 -0
- package/pages/overview.vue +51 -33
- package/pages/play.vue +4 -4
- package/pages/presenter.vue +9 -7
- package/state/index.ts +1 -1
- package/styles/code.css +1 -1
- package/styles/index.css +50 -1
- /package/internals/{Editor.vue → SideEditor.vue} +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
|
|
3
|
+
import type { KeyedTokensInfo } from 'shiki-magic-move/types'
|
|
4
|
+
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
|
5
|
+
import { useSlideContext } from '../context'
|
|
6
|
+
import { makeId } from '../logic/utils'
|
|
7
|
+
|
|
8
|
+
import 'shiki-magic-move/style.css'
|
|
9
|
+
|
|
10
|
+
const props = defineProps<{
|
|
11
|
+
steps: KeyedTokensInfo[]
|
|
12
|
+
at?: string | number
|
|
13
|
+
}>()
|
|
14
|
+
|
|
15
|
+
const { $clicksContext: clicks, $scale: scale } = useSlideContext()
|
|
16
|
+
const id = makeId()
|
|
17
|
+
const index = ref(0)
|
|
18
|
+
|
|
19
|
+
onUnmounted(() => {
|
|
20
|
+
clicks!.unregister(id)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
onMounted(() => {
|
|
24
|
+
if (!clicks || clicks.disabled)
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
const { start, end, delta } = clicks.resolve(props.at || '+1', props.steps.length - 1)
|
|
28
|
+
clicks.register(id, { max: end, delta })
|
|
29
|
+
|
|
30
|
+
watchEffect(() => {
|
|
31
|
+
if (clicks.disabled)
|
|
32
|
+
index.value = props.steps.length - 1
|
|
33
|
+
else
|
|
34
|
+
index.value = Math.min(Math.max(0, clicks.current - start + 1), props.steps.length - 1)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<div class="slidev-code-wrapper slidev-code-magic-move">
|
|
41
|
+
<ShikiMagicMovePrecompiled
|
|
42
|
+
class="slidev-code relative shiki"
|
|
43
|
+
:steps="steps"
|
|
44
|
+
:step="index"
|
|
45
|
+
:options="{ globalScale: scale }"
|
|
46
|
+
/>
|
|
47
|
+
</div>
|
|
48
|
+
</template>
|
package/builtin/VClick.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { PropType, VNode } from 'vue'
|
|
8
8
|
import { Text, defineComponent, h } from 'vue'
|
|
9
|
+
import { CLICKS_MAX } from '../constants'
|
|
9
10
|
import VClicks from './VClicks'
|
|
10
11
|
|
|
11
12
|
export default defineComponent({
|
|
@@ -31,7 +32,7 @@ export default defineComponent({
|
|
|
31
32
|
return h(
|
|
32
33
|
VClicks,
|
|
33
34
|
{
|
|
34
|
-
every:
|
|
35
|
+
every: CLICKS_MAX,
|
|
35
36
|
at: this.at,
|
|
36
37
|
hide: this.hide,
|
|
37
38
|
fade: this.fade,
|
package/composables/useClicks.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { sum } from '@antfu/utils'
|
|
2
2
|
import type { ClicksContext } from '@slidev/types'
|
|
3
3
|
import type { Ref } from 'vue'
|
|
4
|
-
import { ref, shallowReactive } from 'vue'
|
|
4
|
+
import { computed, ref, shallowReactive } from 'vue'
|
|
5
5
|
import type { RouteRecordRaw } from 'vue-router'
|
|
6
6
|
import { currentRoute, isPrintMode, isPrintWithClicks, queryClicks, routeForceRefresh } from '../logic/nav'
|
|
7
7
|
import { normalizeAtProp } from '../logic/utils'
|
|
8
|
+
import { CLICKS_MAX } from '../constants'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
* @internal
|
|
11
|
-
*/
|
|
12
|
-
export function useClicksContextBase(getCurrent: () => number, clicksOverrides?: number): ClicksContext {
|
|
10
|
+
function useClicksContextBase(current: Ref<number>, clicksOverrides?: number): ClicksContext {
|
|
13
11
|
const relativeOffsets: ClicksContext['relativeOffsets'] = new Map()
|
|
14
12
|
const map: ClicksContext['map'] = shallowReactive(new Map())
|
|
15
13
|
|
|
@@ -18,7 +16,10 @@ export function useClicksContextBase(getCurrent: () => number, clicksOverrides?:
|
|
|
18
16
|
return isPrintMode.value && !isPrintWithClicks.value
|
|
19
17
|
},
|
|
20
18
|
get current() {
|
|
21
|
-
return
|
|
19
|
+
return current.value
|
|
20
|
+
},
|
|
21
|
+
set current(value) {
|
|
22
|
+
current.value = value
|
|
22
23
|
},
|
|
23
24
|
relativeOffsets,
|
|
24
25
|
map,
|
|
@@ -65,17 +66,25 @@ export function useClicksContextBase(getCurrent: () => number, clicksOverrides?:
|
|
|
65
66
|
export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksContext {
|
|
66
67
|
if (route?.meta?.__clicksContext)
|
|
67
68
|
return route.meta.__clicksContext
|
|
68
|
-
const thisPath = +(route?.path ??
|
|
69
|
-
const
|
|
70
|
-
()
|
|
71
|
-
const currentPath = +(currentRoute.value?.path ??
|
|
69
|
+
const thisPath = +(route?.path ?? CLICKS_MAX)
|
|
70
|
+
const current = computed({
|
|
71
|
+
get() {
|
|
72
|
+
const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
|
|
72
73
|
if (currentPath === thisPath)
|
|
73
74
|
return queryClicks.value
|
|
74
75
|
else if (currentPath > thisPath)
|
|
75
|
-
return
|
|
76
|
+
return CLICKS_MAX
|
|
76
77
|
else
|
|
77
78
|
return 0
|
|
78
79
|
},
|
|
80
|
+
set(v) {
|
|
81
|
+
const currentPath = +(currentRoute.value?.path ?? CLICKS_MAX)
|
|
82
|
+
if (currentPath === thisPath)
|
|
83
|
+
queryClicks.value = v
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
const context = useClicksContextBase(
|
|
87
|
+
current,
|
|
79
88
|
route?.meta?.clicks,
|
|
80
89
|
)
|
|
81
90
|
if (route?.meta)
|
|
@@ -83,7 +92,6 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
83
92
|
return context
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0):
|
|
87
|
-
|
|
88
|
-
return [current, useClicksContextBase(() => current.value, route?.meta?.clicks)]
|
|
95
|
+
export function useFixedClicks(route?: RouteRecordRaw | undefined, currentInit = 0): ClicksContext {
|
|
96
|
+
return useClicksContextBase(ref(currentInit), route?.meta?.clicks)
|
|
89
97
|
}
|
package/constants.ts
CHANGED
|
@@ -22,6 +22,8 @@ export const CLASS_VCLICK_HIDDEN_EXP = 'slidev-vclick-hidden-explicitly'
|
|
|
22
22
|
export const CLASS_VCLICK_CURRENT = 'slidev-vclick-current'
|
|
23
23
|
export const CLASS_VCLICK_PRIOR = 'slidev-vclick-prior'
|
|
24
24
|
|
|
25
|
+
export const CLICKS_MAX = 999999
|
|
26
|
+
|
|
25
27
|
export const TRUST_ORIGINS = [
|
|
26
28
|
'localhost',
|
|
27
29
|
'127.0.0.1',
|
package/context.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { shallowRef, toRef } from 'vue'
|
|
1
|
+
import { ref, shallowRef, toRef } from 'vue'
|
|
2
2
|
import { injectLocal, objectOmit, provideLocal } from '@vueuse/core'
|
|
3
3
|
import { useFixedClicks } from './composables/useClicks'
|
|
4
4
|
import {
|
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
injectionFrontmatter,
|
|
10
10
|
injectionRenderContext,
|
|
11
11
|
injectionRoute,
|
|
12
|
+
injectionSlideScale,
|
|
12
13
|
injectionSlidevContext,
|
|
13
14
|
} from './constants'
|
|
14
15
|
|
|
15
|
-
const clicksContextFallback = shallowRef(useFixedClicks()
|
|
16
|
+
const clicksContextFallback = shallowRef(useFixedClicks())
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Get the current slide context, should be called inside the setup function of a component inside slide
|
|
@@ -26,6 +27,7 @@ export function useSlideContext() {
|
|
|
26
27
|
const $renderContext = injectLocal(injectionRenderContext)!
|
|
27
28
|
const $frontmatter = injectLocal(injectionFrontmatter, {})
|
|
28
29
|
const $route = injectLocal(injectionRoute, undefined)
|
|
30
|
+
const $scale = injectLocal(injectionSlideScale, ref(1))!
|
|
29
31
|
|
|
30
32
|
return {
|
|
31
33
|
$slidev,
|
|
@@ -36,6 +38,7 @@ export function useSlideContext() {
|
|
|
36
38
|
$route,
|
|
37
39
|
$renderContext,
|
|
38
40
|
$frontmatter,
|
|
41
|
+
$scale,
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
-
import { injectLocal } from '@vueuse/core'
|
|
4
3
|
import { drauu, drawingEnabled, loadCanvas } from '../logic/drawings'
|
|
5
|
-
import {
|
|
4
|
+
import { useSlideContext } from '../context'
|
|
6
5
|
|
|
7
|
-
const scale =
|
|
6
|
+
const scale = useSlideContext().$scale
|
|
8
7
|
const svg = ref<SVGSVGElement>()
|
|
9
8
|
|
|
10
9
|
onMounted(() => {
|
|
@@ -1,19 +1,125 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
|
3
|
+
import type { ClicksContext } from '@slidev/types'
|
|
4
|
+
import { CLICKS_MAX } from '../constants'
|
|
5
|
+
|
|
2
6
|
const props = defineProps<{
|
|
3
7
|
class?: string
|
|
4
8
|
noteHtml?: string
|
|
5
9
|
note?: string
|
|
6
10
|
placeholder?: string
|
|
11
|
+
clicksContext?: ClicksContext
|
|
7
12
|
}>()
|
|
8
13
|
|
|
9
14
|
defineEmits(['click'])
|
|
15
|
+
|
|
16
|
+
const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
|
|
17
|
+
const noteDisplay = ref<HTMLElement | null>(null)
|
|
18
|
+
|
|
19
|
+
const CLASS_FADE = 'slidev-note-fade'
|
|
20
|
+
const CLASS_MARKER = 'slidev-note-click-mark'
|
|
21
|
+
|
|
22
|
+
function highlightNote() {
|
|
23
|
+
if (!noteDisplay.value || !withClicks.value || props.clicksContext?.current == null)
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
const current = +props.clicksContext?.current ?? CLICKS_MAX
|
|
27
|
+
const disabled = current < 0 || current >= CLICKS_MAX
|
|
28
|
+
if (disabled) {
|
|
29
|
+
Array.from(noteDisplay.value.querySelectorAll('*'))
|
|
30
|
+
.forEach(el => el.classList.remove(CLASS_FADE))
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const nodeToIgnores = new Set<Element>()
|
|
35
|
+
function ignoreParent(node: Element) {
|
|
36
|
+
if (!node || node === noteDisplay.value)
|
|
37
|
+
return
|
|
38
|
+
nodeToIgnores.add(node)
|
|
39
|
+
if (node.parentElement)
|
|
40
|
+
ignoreParent(node.parentElement)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
|
|
44
|
+
const markersMap = new Map<number, HTMLElement>()
|
|
45
|
+
|
|
46
|
+
// Convert all sibling text nodes to spans, so we attach classes to them
|
|
47
|
+
for (const marker of markers) {
|
|
48
|
+
const parent = marker.parentElement!
|
|
49
|
+
const clicks = Number(marker.dataset!.clicks)
|
|
50
|
+
markersMap.set(clicks, marker)
|
|
51
|
+
// Ignore the parents of the marker, so the class only applies to the children
|
|
52
|
+
ignoreParent(parent)
|
|
53
|
+
Array.from(parent!.childNodes)
|
|
54
|
+
.forEach((node) => {
|
|
55
|
+
if (node.nodeType === 3) { // text node
|
|
56
|
+
const span = document.createElement('span')
|
|
57
|
+
span.textContent = node.textContent
|
|
58
|
+
parent.insertBefore(span, node)
|
|
59
|
+
node.remove()
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
const children = Array.from(noteDisplay.value.querySelectorAll('*'))
|
|
64
|
+
|
|
65
|
+
let count = 0
|
|
66
|
+
|
|
67
|
+
// Segmenting notes by clicks
|
|
68
|
+
const segments = new Map<number, Element[]>()
|
|
69
|
+
for (const child of children) {
|
|
70
|
+
if (!segments.has(count))
|
|
71
|
+
segments.set(count, [])
|
|
72
|
+
segments.get(count)!.push(child)
|
|
73
|
+
// Update count when reach marker
|
|
74
|
+
if (child.classList.contains(CLASS_MARKER))
|
|
75
|
+
count = Number((child as HTMLElement).dataset.clicks) || (count + 1)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Apply
|
|
79
|
+
for (const [count, els] of segments) {
|
|
80
|
+
els.forEach(el => el.classList.toggle(
|
|
81
|
+
CLASS_FADE,
|
|
82
|
+
nodeToIgnores.has(el)
|
|
83
|
+
? false
|
|
84
|
+
: count !== current,
|
|
85
|
+
))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const [clicks, marker] of markersMap) {
|
|
89
|
+
marker.classList.remove(CLASS_FADE)
|
|
90
|
+
marker.classList.toggle(`${CLASS_MARKER}-past`, clicks < current)
|
|
91
|
+
marker.classList.toggle(`${CLASS_MARKER}-active`, clicks === current)
|
|
92
|
+
marker.classList.toggle(`${CLASS_MARKER}-next`, clicks === current + 1)
|
|
93
|
+
marker.classList.toggle(`${CLASS_MARKER}-future`, clicks > current + 1)
|
|
94
|
+
marker.addEventListener('dblclick', (e) => {
|
|
95
|
+
props.clicksContext!.current = clicks
|
|
96
|
+
e.stopPropagation()
|
|
97
|
+
e.stopImmediatePropagation()
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
watch(
|
|
103
|
+
() => [props.noteHtml, props.clicksContext?.current],
|
|
104
|
+
() => {
|
|
105
|
+
nextTick(() => {
|
|
106
|
+
highlightNote()
|
|
107
|
+
})
|
|
108
|
+
},
|
|
109
|
+
{ immediate: true },
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
highlightNote()
|
|
114
|
+
})
|
|
10
115
|
</script>
|
|
11
116
|
|
|
12
117
|
<template>
|
|
13
118
|
<div
|
|
14
119
|
v-if="noteHtml"
|
|
15
|
-
|
|
16
|
-
|
|
120
|
+
ref="noteDisplay"
|
|
121
|
+
class="prose overflow-auto outline-none slidev-note"
|
|
122
|
+
:class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
|
|
17
123
|
@click="$emit('click')"
|
|
18
124
|
v-html="noteHtml"
|
|
19
125
|
/>
|
package/internals/NoteEditor.vue
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { PropType } from 'vue'
|
|
2
3
|
import { nextTick, ref, watch, watchEffect } from 'vue'
|
|
3
4
|
import { ignorableWatch, onClickOutside, useVModel } from '@vueuse/core'
|
|
5
|
+
import type { ClicksContext } from '@slidev/types'
|
|
4
6
|
import { useDynamicSlideInfo } from '../logic/note'
|
|
5
7
|
import NoteDisplay from './NoteDisplay.vue'
|
|
6
8
|
|
|
@@ -20,6 +22,9 @@ const props = defineProps({
|
|
|
20
22
|
placeholder: {
|
|
21
23
|
default: 'No notes for this slide',
|
|
22
24
|
},
|
|
25
|
+
clicksContext: {
|
|
26
|
+
type: Object as PropType<ClicksContext>,
|
|
27
|
+
},
|
|
23
28
|
autoHeight: {
|
|
24
29
|
default: false,
|
|
25
30
|
},
|
|
@@ -100,6 +105,7 @@ watch(
|
|
|
100
105
|
:style="props.style"
|
|
101
106
|
:note="note || placeholder"
|
|
102
107
|
:note-html="info?.noteHTML"
|
|
108
|
+
:clicks-context="clicksContext"
|
|
103
109
|
/>
|
|
104
110
|
<textarea
|
|
105
111
|
v-else
|
package/internals/NoteStatic.vue
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import type { ClicksContext } from 'packages/types'
|
|
2
3
|
import { useSlideInfo } from '../logic/note'
|
|
3
4
|
import NoteDisplay from './NoteDisplay.vue'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
no?: number
|
|
7
8
|
class?: string
|
|
9
|
+
clicksContext?: ClicksContext
|
|
8
10
|
}>()
|
|
9
11
|
|
|
10
12
|
const { info } = useSlideInfo(props.no)
|
|
@@ -15,5 +17,6 @@ const { info } = useSlideInfo(props.no)
|
|
|
15
17
|
:class="props.class"
|
|
16
18
|
:note="info?.note"
|
|
17
19
|
:note-html="info?.noteHTML"
|
|
20
|
+
:clicks-context="clicksContext"
|
|
18
21
|
/>
|
|
19
22
|
</template>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ClicksContext } from '@slidev/types'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { CLICKS_MAX } from '../constants'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
clicksContext: ClicksContext
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const total = computed(() => props.clicksContext.total)
|
|
11
|
+
const current = computed({
|
|
12
|
+
get() {
|
|
13
|
+
return props.clicksContext.current > total.value ? -1 : props.clicksContext.current
|
|
14
|
+
},
|
|
15
|
+
set(value: number) {
|
|
16
|
+
// eslint-disable-next-line vue/no-mutating-props
|
|
17
|
+
props.clicksContext.current = value
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const range = computed(() => Array.from({ length: total.value + 1 }, (_, i) => i))
|
|
22
|
+
|
|
23
|
+
function onMousedown() {
|
|
24
|
+
if (current.value < 0 || current.value > total.value)
|
|
25
|
+
current.value = 0
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<template>
|
|
30
|
+
<div
|
|
31
|
+
class="flex gap-0.5 items-center select-none"
|
|
32
|
+
:title="`Clicks in this slide: ${total}`"
|
|
33
|
+
>
|
|
34
|
+
<div class="flex gap-1 items-center min-w-16">
|
|
35
|
+
<carbon:cursor-1 text-sm op50 />
|
|
36
|
+
<span v-if="current <= total && current >= 0" text-primary>{{ current }}/</span>
|
|
37
|
+
<span op50>{{ total }}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div
|
|
40
|
+
relative flex-auto h5 flex="~"
|
|
41
|
+
@dblclick="current = CLICKS_MAX"
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
v-for="i of range" :key="i"
|
|
45
|
+
border="y main" of-hidden relative
|
|
46
|
+
:class="[
|
|
47
|
+
i === 0 ? 'rounded-l border-l' : '',
|
|
48
|
+
i === total ? 'rounded-r border-r' : '',
|
|
49
|
+
]"
|
|
50
|
+
:style="{ width: `${1 / total * 100}%` }"
|
|
51
|
+
>
|
|
52
|
+
<div absolute inset-0 z--1 :class=" i <= current ? 'bg-primary op20' : ''" />
|
|
53
|
+
<div
|
|
54
|
+
:class="[
|
|
55
|
+
+i === +current ? 'text-primary font-bold op100 border-primary' : 'op30 border-main',
|
|
56
|
+
i === 0 ? 'rounded-l' : '',
|
|
57
|
+
i === total ? 'rounded-r' : 'border-r-2',
|
|
58
|
+
]"
|
|
59
|
+
w-full h-full text-xs flex items-center justify-center
|
|
60
|
+
>
|
|
61
|
+
{{ i }}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<input
|
|
65
|
+
v-model="current"
|
|
66
|
+
class="range" absolute inset-0
|
|
67
|
+
type="range" :min="0" :max="total" :step="1" z-10 op0
|
|
68
|
+
:style="{ '--thumb-width': `${1 / (total + 1) * 100}%` }"
|
|
69
|
+
@mousedown="onMousedown"
|
|
70
|
+
>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<style scoped>
|
|
76
|
+
.range {
|
|
77
|
+
-webkit-appearance: none;
|
|
78
|
+
appearance: none;
|
|
79
|
+
background: transparent;
|
|
80
|
+
}
|
|
81
|
+
.range::-webkit-slider-thumb {
|
|
82
|
+
-webkit-appearance: none;
|
|
83
|
+
height: 100%;
|
|
84
|
+
width: var(--thumb-width, 0.5rem);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.range::-moz-range-thumb {
|
|
88
|
+
height: 100%;
|
|
89
|
+
width: var(--thumb-width, 0.5rem);
|
|
90
|
+
}
|
|
91
|
+
</style>
|
package/internals/PrintSlide.vue
CHANGED
|
@@ -9,7 +9,7 @@ const props = defineProps<{ route: RouteRecordRaw }>()
|
|
|
9
9
|
|
|
10
10
|
const route = computed(() => props.route)
|
|
11
11
|
const nav = useNav(route)
|
|
12
|
-
const clicks0 = useFixedClicks(route.value, 0)
|
|
12
|
+
const clicks0 = useFixedClicks(route.value, 0)
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<template>
|
|
@@ -19,6 +19,12 @@ const clicks0 = useFixedClicks(route.value, 0)[1]
|
|
|
19
19
|
:route="route"
|
|
20
20
|
/>
|
|
21
21
|
<template v-if="!clicks0.disabled">
|
|
22
|
-
<PrintSlideClick
|
|
22
|
+
<PrintSlideClick
|
|
23
|
+
v-for="i of clicks0.total"
|
|
24
|
+
:key="i"
|
|
25
|
+
:clicks-context="useFixedClicks(route, i)"
|
|
26
|
+
:nav="nav"
|
|
27
|
+
:route="route"
|
|
28
|
+
/>
|
|
23
29
|
</template>
|
|
24
30
|
</template>
|
|
@@ -7,6 +7,7 @@ import { currentPage, go as goSlide, rawRoutes } from '../logic/nav'
|
|
|
7
7
|
import { currentOverviewPage, overviewRowCount } from '../logic/overview'
|
|
8
8
|
import { useFixedClicks } from '../composables/useClicks'
|
|
9
9
|
import { getSlideClass } from '../utils'
|
|
10
|
+
import { CLICKS_MAX } from '../constants'
|
|
10
11
|
import SlideContainer from './SlideContainer.vue'
|
|
11
12
|
import SlideWrapper from './SlideWrapper'
|
|
12
13
|
import DrawingPreview from './DrawingPreview.vue'
|
|
@@ -139,7 +140,7 @@ watchEffect(() => {
|
|
|
139
140
|
<SlideWrapper
|
|
140
141
|
:is="route.component"
|
|
141
142
|
v-if="route?.component"
|
|
142
|
-
:clicks-context="useFixedClicks(route,
|
|
143
|
+
:clicks-context="useFixedClicks(route, CLICKS_MAX)"
|
|
143
144
|
:class="getSlideClass(route)"
|
|
144
145
|
:route="route"
|
|
145
146
|
render-context="overview"
|
package/logic/nav.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { rawRoutes, router } from '../routes'
|
|
|
7
7
|
import { configs } from '../env'
|
|
8
8
|
import { skipTransition } from '../composables/hmr'
|
|
9
9
|
import { usePrimaryClicks } from '../composables/useClicks'
|
|
10
|
+
import { CLICKS_MAX } from '../constants'
|
|
10
11
|
import { useRouteQuery } from './route'
|
|
11
12
|
import { isDrawing } from './drawings'
|
|
12
13
|
|
|
@@ -39,7 +40,7 @@ export const queryClicks = computed({
|
|
|
39
40
|
get() {
|
|
40
41
|
// eslint-disable-next-line ts/no-use-before-define
|
|
41
42
|
if (clicksContext.value.disabled)
|
|
42
|
-
return
|
|
43
|
+
return CLICKS_MAX
|
|
43
44
|
let v = +(queryClicksRaw.value || 0)
|
|
44
45
|
if (Number.isNaN(v))
|
|
45
46
|
v = 0
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.48.0-beta.
|
|
4
|
+
"version": "0.48.0-beta.13",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -51,11 +51,12 @@
|
|
|
51
51
|
"prettier": "^3.2.5",
|
|
52
52
|
"recordrtc": "^5.6.2",
|
|
53
53
|
"resolve": "^1.22.8",
|
|
54
|
+
"shiki-magic-move": "^0.1.0",
|
|
54
55
|
"unocss": "^0.58.5",
|
|
55
|
-
"vue": "^3.4.
|
|
56
|
+
"vue": "^3.4.20",
|
|
56
57
|
"vue-router": "^4.3.0",
|
|
57
|
-
"@slidev/
|
|
58
|
-
"@slidev/
|
|
58
|
+
"@slidev/types": "0.48.0-beta.13",
|
|
59
|
+
"@slidev/parser": "0.48.0-beta.13"
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
61
62
|
"vite": "^5.1.4"
|
package/pages/notes.vue
CHANGED
|
@@ -51,6 +51,7 @@ function decreaseFontSize() {
|
|
|
51
51
|
:note="currentRoute?.meta?.slide?.note"
|
|
52
52
|
:note-html="currentRoute?.meta?.slide?.noteHTML"
|
|
53
53
|
:placeholder="`No notes for Slide ${pageNo}.`"
|
|
54
|
+
:clicks-context="currentRoute?.meta?.__clicksContext"
|
|
54
55
|
/>
|
|
55
56
|
</div>
|
|
56
57
|
<div class="flex-none border-t border-main">
|
package/pages/overview.vue
CHANGED
|
@@ -4,8 +4,8 @@ import { useHead } from '@unhead/vue'
|
|
|
4
4
|
import type { RouteRecordRaw } from 'vue-router'
|
|
5
5
|
import type { ClicksContext } from 'packages/types'
|
|
6
6
|
import { themeVars } from '../env'
|
|
7
|
-
import { rawRoutes } from '../logic/nav'
|
|
8
|
-
import {
|
|
7
|
+
import { openInEditor, rawRoutes } from '../logic/nav'
|
|
8
|
+
import { useFixedClicks } from '../composables/useClicks'
|
|
9
9
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
10
10
|
import { getSlideClass } from '../utils'
|
|
11
11
|
import SlideContainer from '../internals/SlideContainer.vue'
|
|
@@ -13,6 +13,8 @@ import SlideWrapper from '../internals/SlideWrapper'
|
|
|
13
13
|
import DrawingPreview from '../internals/DrawingPreview.vue'
|
|
14
14
|
import IconButton from '../internals/IconButton.vue'
|
|
15
15
|
import NoteEditor from '../internals/NoteEditor.vue'
|
|
16
|
+
import OverviewClicksSlider from '../internals/OverviewClicksSlider.vue'
|
|
17
|
+
import { CLICKS_MAX } from '../constants'
|
|
16
18
|
|
|
17
19
|
const cardWidth = 450
|
|
18
20
|
|
|
@@ -28,15 +30,15 @@ const totalWords = computed(() => wordCounts.value.reduce((a, b) => a + b, 0))
|
|
|
28
30
|
const totalClicks = computed(() => rawRoutes.map(route => getSlideClicks(route)).reduce((a, b) => a + b, 0))
|
|
29
31
|
|
|
30
32
|
const clicksContextMap = new WeakMap<RouteRecordRaw, ClicksContext>()
|
|
31
|
-
function
|
|
33
|
+
function getClicksContext(route: RouteRecordRaw) {
|
|
32
34
|
// We create a local clicks context to calculate the total clicks of the slide
|
|
33
35
|
if (!clicksContextMap.has(route))
|
|
34
|
-
clicksContextMap.set(route,
|
|
36
|
+
clicksContextMap.set(route, useFixedClicks(route, CLICKS_MAX))
|
|
35
37
|
return clicksContextMap.get(route)!
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function getSlideClicks(route: RouteRecordRaw) {
|
|
39
|
-
return route.meta?.clicks ||
|
|
41
|
+
return route.meta?.clicks || getClicksContext(route)?.total
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function wordCount(str: string) {
|
|
@@ -129,40 +131,55 @@ onMounted(() => {
|
|
|
129
131
|
:ref="el => blocks.set(idx, el as any)"
|
|
130
132
|
class="relative border-t border-main of-hidden flex gap-4 min-h-50 group"
|
|
131
133
|
>
|
|
132
|
-
<div class="select-none w-13 text-right my4">
|
|
134
|
+
<div class="select-none w-13 text-right my4 flex flex-col gap-1 items-end">
|
|
133
135
|
<div class="text-3xl op20 mb2">
|
|
134
136
|
{{ idx + 1 }}
|
|
135
137
|
</div>
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
<IconButton
|
|
139
|
+
class="mr--3 op0 group-hover:op80"
|
|
140
|
+
title="Play in new tab"
|
|
141
|
+
@click="openSlideInNewTab(route.path)"
|
|
140
142
|
>
|
|
141
|
-
<carbon:
|
|
142
|
-
|
|
143
|
-
|
|
143
|
+
<carbon:presentation-file />
|
|
144
|
+
</IconButton>
|
|
145
|
+
<IconButton
|
|
146
|
+
v-if="route.meta?.slide"
|
|
147
|
+
class="mr--3 op0 group-hover:op80"
|
|
148
|
+
title="Open in editor"
|
|
149
|
+
@click="openInEditor(`${route.meta.slide.filepath}:${route.meta.slide.start}`)"
|
|
150
|
+
>
|
|
151
|
+
<carbon:cics-program />
|
|
152
|
+
</IconButton>
|
|
144
153
|
</div>
|
|
145
|
-
<div
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
<SlideContainer
|
|
151
|
-
:key="route.path"
|
|
152
|
-
:width="cardWidth"
|
|
153
|
-
:clicks-disabled="true"
|
|
154
|
-
class="pointer-events-none important:[&_*]:select-none"
|
|
154
|
+
<div class="flex flex-col gap-2 my5">
|
|
155
|
+
<div
|
|
156
|
+
class="border rounded border-main overflow-hidden bg-main select-none h-max"
|
|
157
|
+
:style="themeVars"
|
|
158
|
+
@dblclick="openSlideInNewTab(route.path)"
|
|
155
159
|
>
|
|
156
|
-
<
|
|
157
|
-
:
|
|
158
|
-
|
|
159
|
-
:clicks-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
<SlideContainer
|
|
161
|
+
:key="route.path"
|
|
162
|
+
:width="cardWidth"
|
|
163
|
+
:clicks-disabled="true"
|
|
164
|
+
class="pointer-events-none important:[&_*]:select-none"
|
|
165
|
+
>
|
|
166
|
+
<SlideWrapper
|
|
167
|
+
:is="route.component"
|
|
168
|
+
v-if="route?.component"
|
|
169
|
+
:clicks-context="getClicksContext(route)"
|
|
170
|
+
:class="getSlideClass(route)"
|
|
171
|
+
:route="route"
|
|
172
|
+
render-context="overview"
|
|
173
|
+
/>
|
|
174
|
+
<DrawingPreview :page="+route.path" />
|
|
175
|
+
</SlideContainer>
|
|
176
|
+
</div>
|
|
177
|
+
<OverviewClicksSlider
|
|
178
|
+
v-if="getSlideClicks(route)"
|
|
179
|
+
mt-2
|
|
180
|
+
:clicks-context="getClicksContext(route)"
|
|
181
|
+
class="w-full"
|
|
182
|
+
/>
|
|
166
183
|
</div>
|
|
167
184
|
<div class="py3 mt-0.5 mr--8 ml--4 op0 transition group-hover:op100">
|
|
168
185
|
<IconButton
|
|
@@ -179,6 +196,7 @@ onMounted(() => {
|
|
|
179
196
|
class="max-w-250 w-250 text-lg rounded p3"
|
|
180
197
|
:auto-height="true"
|
|
181
198
|
:editing="edittingNote === idx"
|
|
199
|
+
:clicks-context="getClicksContext(route)"
|
|
182
200
|
@dblclick="edittingNote !== idx ? edittingNote = idx : null"
|
|
183
201
|
@update:editing="edittingNote = null"
|
|
184
202
|
/>
|
package/pages/play.vue
CHANGED
|
@@ -31,9 +31,9 @@ useSwipeControls(root)
|
|
|
31
31
|
|
|
32
32
|
const persistNav = computed(() => isScreenVertical.value || showEditor.value)
|
|
33
33
|
|
|
34
|
-
const
|
|
34
|
+
const SideEditor = shallowRef<any>()
|
|
35
35
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
36
|
-
import('../internals/
|
|
36
|
+
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
37
37
|
|
|
38
38
|
const DrawingControls = shallowRef<any>()
|
|
39
39
|
if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
@@ -70,8 +70,8 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
70
70
|
</template>
|
|
71
71
|
</SlideContainer>
|
|
72
72
|
|
|
73
|
-
<template v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ &&
|
|
74
|
-
<
|
|
73
|
+
<template v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && SideEditor && showEditor">
|
|
74
|
+
<SideEditor :resize="true" />
|
|
75
75
|
</template>
|
|
76
76
|
</div>
|
|
77
77
|
<Controls />
|
package/pages/presenter.vue
CHANGED
|
@@ -49,12 +49,12 @@ const nextFrameClicksCtx = computed(() => {
|
|
|
49
49
|
return nextFrame.value && clicksCtxMap[+nextFrame.value[0].path - 1]
|
|
50
50
|
})
|
|
51
51
|
watch([currentRoute, queryClicks], () => {
|
|
52
|
-
nextFrameClicksCtx.value && (nextFrameClicksCtx.value
|
|
52
|
+
nextFrameClicksCtx.value && (nextFrameClicksCtx.value.current = nextFrame.value![1])
|
|
53
53
|
}, { immediate: true })
|
|
54
54
|
|
|
55
|
-
const
|
|
55
|
+
const SideEditor = shallowRef<any>()
|
|
56
56
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
57
|
-
import('../internals/
|
|
57
|
+
import('../internals/SideEditor.vue').then(v => SideEditor.value = v.default)
|
|
58
58
|
|
|
59
59
|
// sync presenter cursor
|
|
60
60
|
onMounted(() => {
|
|
@@ -121,9 +121,9 @@ onMounted(() => {
|
|
|
121
121
|
class="h-full w-full"
|
|
122
122
|
>
|
|
123
123
|
<SlideWrapper
|
|
124
|
-
:is="nextFrame[0].component as any"
|
|
124
|
+
:is="(nextFrame[0].component as any)"
|
|
125
125
|
:key="nextFrame[0].path"
|
|
126
|
-
:clicks-context="nextFrameClicksCtx
|
|
126
|
+
:clicks-context="nextFrameClicksCtx"
|
|
127
127
|
:class="getSlideClass(nextFrame[0])"
|
|
128
128
|
:route="nextFrame[0]"
|
|
129
129
|
render-context="previewNext"
|
|
@@ -134,8 +134,8 @@ onMounted(() => {
|
|
|
134
134
|
</div>
|
|
135
135
|
</div>
|
|
136
136
|
<!-- Notes -->
|
|
137
|
-
<div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ &&
|
|
138
|
-
<
|
|
137
|
+
<div v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__ && SideEditor && showEditor" class="grid-section note of-auto">
|
|
138
|
+
<SideEditor />
|
|
139
139
|
</div>
|
|
140
140
|
<div v-else class="grid-section note grid grid-rows-[1fr_min-content] overflow-hidden">
|
|
141
141
|
<NoteEditor
|
|
@@ -144,6 +144,7 @@ onMounted(() => {
|
|
|
144
144
|
:no="currentSlideId"
|
|
145
145
|
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
|
|
146
146
|
:editing="notesEditing"
|
|
147
|
+
:clicks-context="clicksContext"
|
|
147
148
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
148
149
|
/>
|
|
149
150
|
<NoteStatic
|
|
@@ -152,6 +153,7 @@ onMounted(() => {
|
|
|
152
153
|
:no="currentSlideId"
|
|
153
154
|
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
|
|
154
155
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
156
|
+
:clicks-context="clicksContext"
|
|
155
157
|
/>
|
|
156
158
|
<div class="border-t border-main py-1 px-2 text-sm">
|
|
157
159
|
<IconButton title="Increase font size" @click="increasePresenterFontSize">
|
package/state/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { slideAspect } from '../env'
|
|
|
5
5
|
export const showRecordingDialog = ref(false)
|
|
6
6
|
export const showInfoDialog = ref(false)
|
|
7
7
|
export const showGotoDialog = ref(false)
|
|
8
|
+
export const showOverview = ref(false)
|
|
8
9
|
|
|
9
10
|
export const shortcutsEnabled = ref(true)
|
|
10
11
|
export const breakpoints = useBreakpoints({
|
|
@@ -24,7 +25,6 @@ export const currentCamera = useLocalStorage<string>('slidev-camera', 'default',
|
|
|
24
25
|
export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
|
|
25
26
|
export const slideScale = useLocalStorage<number>('slidev-scale', 0)
|
|
26
27
|
|
|
27
|
-
export const showOverview = useLocalStorage('slidev-show-overview', false, { listenToStorageChanges: false })
|
|
28
28
|
export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
|
|
29
29
|
export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
|
|
30
30
|
export const isEditorVertical = useLocalStorage('slidev-editor-vertical', false, { listenToStorageChanges: false })
|
package/styles/code.css
CHANGED
package/styles/index.css
CHANGED
|
@@ -16,7 +16,11 @@ html {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
.slidev-icon-btn {
|
|
19
|
-
|
|
19
|
+
aspect-ratio: 1;
|
|
20
|
+
display: inline-block;
|
|
21
|
+
user-select: none;
|
|
22
|
+
outline: none;
|
|
23
|
+
cursor: pointer;
|
|
20
24
|
@apply opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
|
|
21
25
|
@apply hover:(opacity-100 bg-gray-400 bg-opacity-10);
|
|
22
26
|
@apply md:p-2;
|
|
@@ -63,6 +67,51 @@ html {
|
|
|
63
67
|
width: 100%;
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
/* Note Clicks */
|
|
71
|
+
|
|
72
|
+
.slidev-note-with-clicks .slidev-note-fade {
|
|
73
|
+
color: #888888ab;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.slidev-note-click-mark {
|
|
77
|
+
user-select: none;
|
|
78
|
+
font-size: 0.7em;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
--uno: text-violet bg-violet/10 px1 font-mono rounded items-center border
|
|
81
|
+
border-transparent;
|
|
82
|
+
}
|
|
83
|
+
.slidev-note-click-mark.slidev-note-click-mark-active {
|
|
84
|
+
--uno: border border-violet;
|
|
85
|
+
}
|
|
86
|
+
.slidev-note-click-mark.slidev-note-click-mark-past {
|
|
87
|
+
filter: saturate(0);
|
|
88
|
+
opacity: 0.5;
|
|
89
|
+
}
|
|
90
|
+
.slidev-note-click-mark.slidev-note-click-mark-future {
|
|
91
|
+
opacity: 0.5;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.slidev-note-click-mark::before {
|
|
95
|
+
content: '';
|
|
96
|
+
display: inline-block;
|
|
97
|
+
--un-icon: url("data:image/svg+xml;utf8,%3Csvg viewBox='0 0 32 32' width='1.2em' height='1.2em' xmlns='http://www.w3.org/2000/svg' %3E%3Cpath fill='currentColor' d='M23 28a1 1 0 0 1-.71-.29l-6.13-6.14l-3.33 5a1 1 0 0 1-1 .44a1 1 0 0 1-.81-.7l-6-20A1 1 0 0 1 6.29 5l20 6a1 1 0 0 1 .7.81a1 1 0 0 1-.44 1l-5 3.33l6.14 6.13a1 1 0 0 1 0 1.42l-4 4A1 1 0 0 1 23 28m0-2.41L25.59 23l-7.16-7.15l5.25-3.5L7.49 7.49l4.86 16.19l3.5-5.25Z'/%3E%3C/svg%3E");
|
|
98
|
+
-webkit-mask: var(--un-icon) no-repeat;
|
|
99
|
+
mask: var(--un-icon) no-repeat;
|
|
100
|
+
-webkit-mask-size: 100% 100%;
|
|
101
|
+
mask-size: 100% 100%;
|
|
102
|
+
background-color: currentColor;
|
|
103
|
+
color: inherit;
|
|
104
|
+
width: 1.2em;
|
|
105
|
+
height: 1.2em;
|
|
106
|
+
opacity: 0.8;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.slidev-note-click-mark::after {
|
|
110
|
+
content: attr(data-clicks);
|
|
111
|
+
display: inline-block;
|
|
112
|
+
transform: translateY(0.1em);
|
|
113
|
+
}
|
|
114
|
+
|
|
66
115
|
/* Transform the position back for Rough Notation (v-mark) */
|
|
67
116
|
.rough-annotation {
|
|
68
117
|
transform: scale(calc(1 / var(--slidev-slide-scale)));
|
|
File without changes
|