@slidev/client 0.48.0-beta.13 → 0.48.0-beta.15
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/Arrow.vue +2 -4
- package/builtin/Mermaid.vue +2 -2
- package/builtin/Monaco.vue +8 -8
- package/builtin/ShikiMagicMove.vue +6 -4
- package/composables/useClicks.ts +4 -2
- package/internals/Controls.vue +2 -2
- package/internals/IconButton.vue +3 -2
- package/internals/NavControls.vue +2 -2
- package/internals/NoteDisplay.vue +43 -26
- package/internals/{NoteEditor.vue → NoteEditable.vue} +31 -27
- package/internals/{SlidesOverview.vue → QuickOverview.vue} +10 -9
- package/internals/RecordingControls.vue +1 -1
- package/modules/mermaid.ts +6 -7
- package/package.json +4 -5
- package/pages/notes.vue +1 -0
- package/pages/overview.vue +15 -4
- package/pages/presenter/print.vue +4 -1
- package/pages/presenter.vue +37 -43
- package/setup/root.ts +2 -2
package/builtin/Arrow.vue
CHANGED
|
@@ -9,7 +9,7 @@ Simple Arrow
|
|
|
9
9
|
-->
|
|
10
10
|
|
|
11
11
|
<script setup lang="ts">
|
|
12
|
-
import {
|
|
12
|
+
import { makeId } from '../logic/utils'
|
|
13
13
|
|
|
14
14
|
defineProps<{
|
|
15
15
|
x1: number | string
|
|
@@ -20,9 +20,7 @@ defineProps<{
|
|
|
20
20
|
color?: string
|
|
21
21
|
}>()
|
|
22
22
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const id = nanoid()
|
|
23
|
+
const id = makeId()
|
|
26
24
|
</script>
|
|
27
25
|
|
|
28
26
|
<template>
|
package/builtin/Mermaid.vue
CHANGED
|
@@ -19,7 +19,7 @@ import ShadowRoot from '../internals/ShadowRoot.vue'
|
|
|
19
19
|
import { isDark } from '../logic/dark'
|
|
20
20
|
|
|
21
21
|
const props = defineProps<{
|
|
22
|
-
|
|
22
|
+
codeLz: string
|
|
23
23
|
scale?: number
|
|
24
24
|
theme?: string
|
|
25
25
|
}>()
|
|
@@ -37,7 +37,7 @@ watchEffect(async (onCleanup) => {
|
|
|
37
37
|
error.value = null
|
|
38
38
|
try {
|
|
39
39
|
const svg = await renderMermaid(
|
|
40
|
-
props.
|
|
40
|
+
props.codeLz || '',
|
|
41
41
|
{
|
|
42
42
|
theme: props.theme || (isDark.value ? 'dark' : undefined),
|
|
43
43
|
...vm!.attrs,
|
package/builtin/Monaco.vue
CHANGED
|
@@ -14,30 +14,30 @@ Learn more: https://sli.dev/guide/syntax.html#monaco-editor
|
|
|
14
14
|
<script setup lang="ts">
|
|
15
15
|
import { computed, onMounted, ref } from 'vue'
|
|
16
16
|
import { useEventListener } from '@vueuse/core'
|
|
17
|
-
import { decode } from 'js-base64'
|
|
18
|
-
import { nanoid } from 'nanoid'
|
|
19
17
|
import type * as monaco from 'monaco-editor'
|
|
18
|
+
import { decompressFromBase64 } from 'lz-string'
|
|
20
19
|
import { isDark } from '../logic/dark'
|
|
20
|
+
import { makeId } from '../logic/utils'
|
|
21
21
|
|
|
22
22
|
const props = withDefaults(defineProps<{
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
codeLz: string
|
|
24
|
+
diffLz?: string
|
|
25
25
|
lang?: string
|
|
26
26
|
readonly?: boolean
|
|
27
27
|
lineNumbers?: 'on' | 'off' | 'relative' | 'interval'
|
|
28
28
|
height?: number | string
|
|
29
29
|
editorOptions?: monaco.editor.IEditorOptions
|
|
30
30
|
}>(), {
|
|
31
|
-
|
|
31
|
+
codeLz: '',
|
|
32
32
|
lang: 'typescript',
|
|
33
33
|
readonly: false,
|
|
34
34
|
lineNumbers: 'off',
|
|
35
35
|
height: 'auto',
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
const id =
|
|
39
|
-
const code = ref(
|
|
40
|
-
const diff = ref(props.
|
|
38
|
+
const id = makeId()
|
|
39
|
+
const code = ref(decompressFromBase64(props.codeLz))
|
|
40
|
+
const diff = ref(props.diffLz ? decompressFromBase64(props.diffLz) : null)
|
|
41
41
|
const lineHeight = +(getComputedStyle(document.body).getPropertyValue('--slidev-code-line-height') || '18').replace('px', '') || 18
|
|
42
42
|
const editorHeight = ref(0)
|
|
43
43
|
const calculatedHeight = computed(() => code.value.split(/\r?\n/g).length * lineHeight)
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
|
|
3
3
|
import type { KeyedTokensInfo } from 'shiki-magic-move/types'
|
|
4
4
|
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
|
5
|
+
import { decompressFromBase64 } from 'lz-string'
|
|
5
6
|
import { useSlideContext } from '../context'
|
|
6
7
|
import { makeId } from '../logic/utils'
|
|
7
8
|
|
|
8
9
|
import 'shiki-magic-move/style.css'
|
|
9
10
|
|
|
10
11
|
const props = defineProps<{
|
|
11
|
-
|
|
12
|
+
stepsLz: string
|
|
12
13
|
at?: string | number
|
|
13
14
|
}>()
|
|
14
15
|
|
|
16
|
+
const steps = JSON.parse(decompressFromBase64(props.stepsLz)) as KeyedTokensInfo[]
|
|
15
17
|
const { $clicksContext: clicks, $scale: scale } = useSlideContext()
|
|
16
18
|
const id = makeId()
|
|
17
19
|
const index = ref(0)
|
|
@@ -24,14 +26,14 @@ onMounted(() => {
|
|
|
24
26
|
if (!clicks || clicks.disabled)
|
|
25
27
|
return
|
|
26
28
|
|
|
27
|
-
const { start, end, delta } = clicks.resolve(props.at || '+1',
|
|
29
|
+
const { start, end, delta } = clicks.resolve(props.at || '+1', steps.length - 1)
|
|
28
30
|
clicks.register(id, { max: end, delta })
|
|
29
31
|
|
|
30
32
|
watchEffect(() => {
|
|
31
33
|
if (clicks.disabled)
|
|
32
|
-
index.value =
|
|
34
|
+
index.value = steps.length - 1
|
|
33
35
|
else
|
|
34
|
-
index.value = Math.min(Math.max(0, clicks.current - start + 1),
|
|
36
|
+
index.value = Math.min(Math.max(0, clicks.current - start + 1), steps.length - 1)
|
|
35
37
|
})
|
|
36
38
|
})
|
|
37
39
|
</script>
|
package/composables/useClicks.ts
CHANGED
|
@@ -69,7 +69,9 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
69
69
|
const thisPath = +(route?.path ?? CLICKS_MAX)
|
|
70
70
|
const current = computed({
|
|
71
71
|
get() {
|
|
72
|
-
const currentPath = +(currentRoute.value?.path ??
|
|
72
|
+
const currentPath = +(currentRoute.value?.path ?? Number.NaN)
|
|
73
|
+
if (!currentPath || Number.isNaN(currentPath))
|
|
74
|
+
return 0
|
|
73
75
|
if (currentPath === thisPath)
|
|
74
76
|
return queryClicks.value
|
|
75
77
|
else if (currentPath > thisPath)
|
|
@@ -78,7 +80,7 @@ export function usePrimaryClicks(route: RouteRecordRaw | undefined): ClicksConte
|
|
|
78
80
|
return 0
|
|
79
81
|
},
|
|
80
82
|
set(v) {
|
|
81
|
-
const currentPath = +(currentRoute.value?.path ??
|
|
83
|
+
const currentPath = +(currentRoute.value?.path ?? Number.NaN)
|
|
82
84
|
if (currentPath === thisPath)
|
|
83
85
|
queryClicks.value = v
|
|
84
86
|
},
|
package/internals/Controls.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { shallowRef } from 'vue'
|
|
3
3
|
import { showInfoDialog, showOverview, showRecordingDialog } from '../state'
|
|
4
4
|
import { configs } from '../env'
|
|
5
|
-
import
|
|
5
|
+
import QuickOverview from './QuickOverview.vue'
|
|
6
6
|
import InfoDialog from './InfoDialog.vue'
|
|
7
7
|
import Goto from './Goto.vue'
|
|
8
8
|
|
|
@@ -15,7 +15,7 @@ if (__SLIDEV_FEATURE_RECORD__) {
|
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
17
|
<template>
|
|
18
|
-
<
|
|
18
|
+
<QuickOverview v-model="showOverview" />
|
|
19
19
|
<Goto />
|
|
20
20
|
<WebCamera v-if="WebCamera" />
|
|
21
21
|
<RecordingDialog v-if="RecordingDialog" v-model="showRecordingDialog" />
|
package/internals/IconButton.vue
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
defineProps<{
|
|
3
3
|
title: string
|
|
4
4
|
icon?: string
|
|
5
|
+
as?: string
|
|
5
6
|
}>()
|
|
6
7
|
</script>
|
|
7
8
|
|
|
8
9
|
<template>
|
|
9
|
-
<button class="slidev-icon-btn" :title="title" v-bind="$attrs">
|
|
10
|
+
<component :is="as || 'button'" class="slidev-icon-btn" :title="title" v-bind="$attrs">
|
|
10
11
|
<span class="sr-only">{{ title }}</span>
|
|
11
12
|
<slot>
|
|
12
13
|
<div :class="icon" />
|
|
13
14
|
</slot>
|
|
14
|
-
</
|
|
15
|
+
</component>
|
|
15
16
|
</template>
|
|
@@ -114,13 +114,13 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
|
|
|
114
114
|
<IconButton
|
|
115
115
|
v-if="__DEV__ && __SLIDEV_FEATURE_EDITOR__"
|
|
116
116
|
:title="showEditor ? 'Hide editor' : 'Show editor'"
|
|
117
|
-
class="
|
|
117
|
+
class="lt-md:hidden"
|
|
118
118
|
@click="showEditor = !showEditor"
|
|
119
119
|
>
|
|
120
120
|
<carbon:text-annotation-toggle />
|
|
121
121
|
</IconButton>
|
|
122
122
|
|
|
123
|
-
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" @click="togglePresenterLayout">
|
|
123
|
+
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial" @click="togglePresenterLayout">
|
|
124
124
|
<carbon:template />
|
|
125
125
|
{{ presenterLayout }}
|
|
126
126
|
</IconButton>
|
|
@@ -9,9 +9,13 @@ const props = defineProps<{
|
|
|
9
9
|
note?: string
|
|
10
10
|
placeholder?: string
|
|
11
11
|
clicksContext?: ClicksContext
|
|
12
|
+
autoScroll?: boolean
|
|
12
13
|
}>()
|
|
13
14
|
|
|
14
|
-
defineEmits
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
(type: 'markerDblclick', e: MouseEvent, clicks: number): void
|
|
17
|
+
(type: 'markerClick', e: MouseEvent, clicks: number): void
|
|
18
|
+
}>()
|
|
15
19
|
|
|
16
20
|
const withClicks = computed(() => props.clicksContext?.current != null && props.noteHtml?.includes('slidev-note-click-mark'))
|
|
17
21
|
const noteDisplay = ref<HTMLElement | null>(null)
|
|
@@ -20,16 +24,13 @@ const CLASS_FADE = 'slidev-note-fade'
|
|
|
20
24
|
const CLASS_MARKER = 'slidev-note-click-mark'
|
|
21
25
|
|
|
22
26
|
function highlightNote() {
|
|
23
|
-
if (!noteDisplay.value || !withClicks.value
|
|
27
|
+
if (!noteDisplay.value || !withClicks.value)
|
|
24
28
|
return
|
|
25
29
|
|
|
26
|
-
const
|
|
30
|
+
const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
|
|
31
|
+
|
|
32
|
+
const current = +(props.clicksContext?.current ?? CLICKS_MAX)
|
|
27
33
|
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
|
|
|
34
35
|
const nodeToIgnores = new Set<Element>()
|
|
35
36
|
function ignoreParent(node: Element) {
|
|
@@ -40,7 +41,6 @@ function highlightNote() {
|
|
|
40
41
|
ignoreParent(node.parentElement)
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
const markers = Array.from(noteDisplay.value.querySelectorAll(`.${CLASS_MARKER}`)) as HTMLElement[]
|
|
44
44
|
const markersMap = new Map<number, HTMLElement>()
|
|
45
45
|
|
|
46
46
|
// Convert all sibling text nodes to spans, so we attach classes to them
|
|
@@ -77,25 +77,39 @@ function highlightNote() {
|
|
|
77
77
|
|
|
78
78
|
// Apply
|
|
79
79
|
for (const [count, els] of segments) {
|
|
80
|
-
|
|
81
|
-
CLASS_FADE
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
if (disabled) {
|
|
81
|
+
els.forEach(el => el.classList.remove(CLASS_FADE))
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
els.forEach(el => el.classList.toggle(
|
|
85
|
+
CLASS_FADE,
|
|
86
|
+
nodeToIgnores.has(el)
|
|
87
|
+
? false
|
|
88
|
+
: count !== current,
|
|
89
|
+
))
|
|
90
|
+
}
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
for (const [clicks, marker] of markersMap) {
|
|
89
94
|
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.
|
|
95
|
+
marker.classList.toggle(`${CLASS_MARKER}-past`, disabled ? false : clicks < current)
|
|
96
|
+
marker.classList.toggle(`${CLASS_MARKER}-active`, disabled ? false : clicks === current)
|
|
97
|
+
marker.classList.toggle(`${CLASS_MARKER}-next`, disabled ? false : clicks === current + 1)
|
|
98
|
+
marker.classList.toggle(`${CLASS_MARKER}-future`, disabled ? false : clicks > current + 1)
|
|
99
|
+
marker.ondblclick = (e) => {
|
|
100
|
+
emit('markerDblclick', e, clicks)
|
|
101
|
+
if (e.defaultPrevented)
|
|
102
|
+
return
|
|
95
103
|
props.clicksContext!.current = clicks
|
|
96
104
|
e.stopPropagation()
|
|
97
105
|
e.stopImmediatePropagation()
|
|
98
|
-
}
|
|
106
|
+
}
|
|
107
|
+
marker.onclick = (e) => {
|
|
108
|
+
emit('markerClick', e, clicks)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (props.autoScroll && clicks === current)
|
|
112
|
+
marker.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
99
113
|
}
|
|
100
114
|
}
|
|
101
115
|
|
|
@@ -120,23 +134,26 @@ onMounted(() => {
|
|
|
120
134
|
ref="noteDisplay"
|
|
121
135
|
class="prose overflow-auto outline-none slidev-note"
|
|
122
136
|
:class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
|
|
123
|
-
@click="$emit('click')"
|
|
124
137
|
v-html="noteHtml"
|
|
125
138
|
/>
|
|
126
139
|
<div
|
|
127
140
|
v-else-if="note"
|
|
128
|
-
class="prose overflow-auto outline-none"
|
|
141
|
+
class="prose overflow-auto outline-none slidev-note"
|
|
129
142
|
:class="props.class"
|
|
130
|
-
@click="$emit('click')"
|
|
131
143
|
>
|
|
132
144
|
<p v-text="note" />
|
|
133
145
|
</div>
|
|
134
146
|
<div
|
|
135
147
|
v-else
|
|
136
|
-
class="prose overflow-auto outline-none opacity-50 italic select-none"
|
|
148
|
+
class="prose overflow-auto outline-none opacity-50 italic select-none slidev-note"
|
|
137
149
|
:class="props.class"
|
|
138
|
-
@click="$emit('click')"
|
|
139
150
|
>
|
|
140
151
|
<p v-text="props.placeholder || 'No notes.'" />
|
|
141
152
|
</div>
|
|
142
153
|
</template>
|
|
154
|
+
|
|
155
|
+
<style>
|
|
156
|
+
.slidev-note :first-child {
|
|
157
|
+
margin-top: 0;
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -30,9 +30,12 @@ const props = defineProps({
|
|
|
30
30
|
},
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
const emit = defineEmits
|
|
34
|
-
'update:editing',
|
|
35
|
-
|
|
33
|
+
const emit = defineEmits<{
|
|
34
|
+
(type: 'update:editing', value: boolean): void
|
|
35
|
+
(type: 'markerDblclick', e: MouseEvent, clicks: number): void
|
|
36
|
+
(type: 'markerClick', e: MouseEvent, clicks: number): void
|
|
37
|
+
}>()
|
|
38
|
+
|
|
36
39
|
const editing = useVModel(props, 'editing', emit, { passive: true })
|
|
37
40
|
|
|
38
41
|
const { info, update } = useDynamicSlideInfo(props.no)
|
|
@@ -40,9 +43,12 @@ const { info, update } = useDynamicSlideInfo(props.no)
|
|
|
40
43
|
const note = ref('')
|
|
41
44
|
let timer: any
|
|
42
45
|
|
|
46
|
+
// Send back the note on changes
|
|
43
47
|
const { ignoreUpdates } = ignorableWatch(
|
|
44
48
|
note,
|
|
45
49
|
(v) => {
|
|
50
|
+
if (!editing.value)
|
|
51
|
+
return
|
|
46
52
|
const id = props.no
|
|
47
53
|
clearTimeout(timer)
|
|
48
54
|
timer = setTimeout(() => {
|
|
@@ -51,72 +57,70 @@ const { ignoreUpdates } = ignorableWatch(
|
|
|
51
57
|
},
|
|
52
58
|
)
|
|
53
59
|
|
|
60
|
+
// Update note value when info changes
|
|
54
61
|
watch(
|
|
55
|
-
info,
|
|
56
|
-
(
|
|
62
|
+
() => info.value?.note,
|
|
63
|
+
(value = '') => {
|
|
57
64
|
if (editing.value)
|
|
58
65
|
return
|
|
59
66
|
clearTimeout(timer)
|
|
60
67
|
ignoreUpdates(() => {
|
|
61
|
-
note.value =
|
|
68
|
+
note.value = value
|
|
62
69
|
})
|
|
63
70
|
},
|
|
64
71
|
{ immediate: true, flush: 'sync' },
|
|
65
72
|
)
|
|
66
73
|
|
|
67
|
-
const
|
|
74
|
+
const inputEl = ref<HTMLTextAreaElement>()
|
|
75
|
+
const inputHeight = ref<number | null>()
|
|
68
76
|
|
|
69
77
|
watchEffect(() => {
|
|
70
78
|
if (editing.value)
|
|
71
|
-
|
|
79
|
+
inputEl.value?.focus()
|
|
72
80
|
})
|
|
73
81
|
|
|
74
|
-
onClickOutside(
|
|
82
|
+
onClickOutside(inputEl, () => {
|
|
75
83
|
editing.value = false
|
|
76
84
|
})
|
|
77
85
|
|
|
78
|
-
function
|
|
79
|
-
if (!props.autoHeight || !
|
|
86
|
+
function calculateEditorHeight() {
|
|
87
|
+
if (!props.autoHeight || !inputEl.value || !editing.value)
|
|
80
88
|
return
|
|
81
|
-
if (
|
|
82
|
-
|
|
89
|
+
if (inputEl.value.scrollHeight > inputEl.value.clientHeight)
|
|
90
|
+
inputEl.value.style.height = `${inputEl.value.scrollHeight}px`
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
const inputHeight = ref<number | null>()
|
|
86
|
-
watchEffect(() => {
|
|
87
|
-
calculateHeight()
|
|
88
|
-
})
|
|
89
93
|
watch(
|
|
90
94
|
note,
|
|
91
|
-
() => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
},
|
|
96
|
-
{ flush: 'post' },
|
|
95
|
+
() => nextTick(() => {
|
|
96
|
+
calculateEditorHeight()
|
|
97
|
+
}),
|
|
98
|
+
{ flush: 'post', immediate: true },
|
|
97
99
|
)
|
|
98
100
|
</script>
|
|
99
101
|
|
|
100
102
|
<template>
|
|
101
103
|
<NoteDisplay
|
|
102
104
|
v-if="!editing"
|
|
103
|
-
class="
|
|
105
|
+
class="border-transparent border-2"
|
|
104
106
|
:class="[props.class, note ? '' : 'opacity-25 italic select-none']"
|
|
105
107
|
:style="props.style"
|
|
106
108
|
:note="note || placeholder"
|
|
107
109
|
:note-html="info?.noteHTML"
|
|
108
110
|
:clicks-context="clicksContext"
|
|
111
|
+
:auto-scroll="!autoHeight"
|
|
112
|
+
@marker-click="(e, clicks) => emit('markerClick', e, clicks)"
|
|
113
|
+
@marker-dblclick="(e, clicks) => emit('markerDblclick', e, clicks)"
|
|
109
114
|
/>
|
|
110
115
|
<textarea
|
|
111
116
|
v-else
|
|
112
|
-
ref="
|
|
117
|
+
ref="inputEl"
|
|
113
118
|
v-model="note"
|
|
114
119
|
class="prose resize-none overflow-auto outline-none bg-transparent block border-primary border-2"
|
|
115
120
|
style="line-height: 1.75;"
|
|
116
121
|
:style="[props.style, inputHeight != null ? { height: `${inputHeight}px` } : {}]"
|
|
117
122
|
:class="props.class"
|
|
118
123
|
:placeholder="placeholder"
|
|
119
|
-
@keydown.esc="
|
|
120
|
-
@focus="editing = true"
|
|
124
|
+
@keydown.esc="editing = false"
|
|
121
125
|
/>
|
|
122
126
|
</template>
|
|
@@ -164,18 +164,19 @@ watchEffect(() => {
|
|
|
164
164
|
</div>
|
|
165
165
|
</div>
|
|
166
166
|
</Transition>
|
|
167
|
-
<div v-if="value" class="fixed top-4 right-4 text-gray-400 flex items-center gap-
|
|
168
|
-
<
|
|
169
|
-
|
|
167
|
+
<div v-if="value" class="fixed top-4 right-4 text-gray-400 flex flex-col items-center gap-2">
|
|
168
|
+
<IconButton title="Close" class="text-2xl" @click="close">
|
|
169
|
+
<carbon:close />
|
|
170
|
+
</IconButton>
|
|
171
|
+
<IconButton
|
|
172
|
+
as="a"
|
|
173
|
+
title="Slides Overview"
|
|
170
174
|
target="_blank"
|
|
171
|
-
|
|
175
|
+
href="/overview"
|
|
172
176
|
tab-index="-1"
|
|
173
|
-
class="
|
|
177
|
+
class="text-2xl"
|
|
174
178
|
>
|
|
175
|
-
|
|
176
|
-
</RouterLink>
|
|
177
|
-
<IconButton title="Close" class="text-2xl" @click="close">
|
|
178
|
-
<carbon:close />
|
|
179
|
+
<carbon:list-boxes />
|
|
179
180
|
</IconButton>
|
|
180
181
|
</div>
|
|
181
182
|
</template>
|
|
@@ -54,7 +54,7 @@ onMounted(() => {
|
|
|
54
54
|
</IconButton>
|
|
55
55
|
<MenuButton :disabled="recording">
|
|
56
56
|
<template #button>
|
|
57
|
-
<IconButton title="Select recording device" class="h-full !text-sm !px-0">
|
|
57
|
+
<IconButton title="Select recording device" class="h-full !text-sm !px-0 aspect-initial">
|
|
58
58
|
<carbon:chevron-up class="opacity-50" />
|
|
59
59
|
</IconButton>
|
|
60
60
|
</template>
|
package/modules/mermaid.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import mermaid from 'mermaid/dist/mermaid.esm.mjs'
|
|
2
|
-
import {
|
|
3
|
-
import { decode } from 'js-base64'
|
|
2
|
+
import { decompressFromBase64 } from 'lz-string'
|
|
4
3
|
import { clearUndefined } from '@antfu/utils'
|
|
5
4
|
import setupMermaid from '../setup/mermaid'
|
|
5
|
+
import { makeId } from '../logic/utils'
|
|
6
6
|
|
|
7
7
|
mermaid.startOnLoad = false
|
|
8
8
|
mermaid.initialize({ startOnLoad: false })
|
|
9
9
|
|
|
10
|
-
const nanoid = customAlphabet('abcedfghicklmn', 10)
|
|
11
10
|
const cache = new Map<string, string>()
|
|
12
11
|
|
|
13
|
-
export async function renderMermaid(
|
|
14
|
-
const key =
|
|
12
|
+
export async function renderMermaid(lzEncoded: string, options: any) {
|
|
13
|
+
const key = lzEncoded + JSON.stringify(options)
|
|
15
14
|
const _cache = cache.get(key)
|
|
16
15
|
if (_cache)
|
|
17
16
|
return _cache
|
|
@@ -21,8 +20,8 @@ export async function renderMermaid(encoded: string, options: any) {
|
|
|
21
20
|
...clearUndefined(setupMermaid() || {}),
|
|
22
21
|
...clearUndefined(options),
|
|
23
22
|
})
|
|
24
|
-
const code =
|
|
25
|
-
const id =
|
|
23
|
+
const code = decompressFromBase64(lzEncoded)
|
|
24
|
+
const id = makeId()
|
|
26
25
|
const { svg } = await mermaid.render(id, code)
|
|
27
26
|
cache.set(key, svg)
|
|
28
27
|
return svg
|
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.15",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -42,12 +42,11 @@
|
|
|
42
42
|
"file-saver": "^2.0.5",
|
|
43
43
|
"floating-vue": "^5.2.2",
|
|
44
44
|
"fuse.js": "^7.0.0",
|
|
45
|
-
"js-base64": "^3.7.7",
|
|
46
45
|
"js-yaml": "^4.1.0",
|
|
47
46
|
"katex": "^0.16.9",
|
|
47
|
+
"lz-string": "^1.5.0",
|
|
48
48
|
"mermaid": "^10.8.0",
|
|
49
49
|
"monaco-editor": "^0.37.1",
|
|
50
|
-
"nanoid": "^5.0.6",
|
|
51
50
|
"prettier": "^3.2.5",
|
|
52
51
|
"recordrtc": "^5.6.2",
|
|
53
52
|
"resolve": "^1.22.8",
|
|
@@ -55,8 +54,8 @@
|
|
|
55
54
|
"unocss": "^0.58.5",
|
|
56
55
|
"vue": "^3.4.20",
|
|
57
56
|
"vue-router": "^4.3.0",
|
|
58
|
-
"@slidev/types": "0.48.0-beta.
|
|
59
|
-
"@slidev/parser": "0.48.0-beta.
|
|
57
|
+
"@slidev/types": "0.48.0-beta.15",
|
|
58
|
+
"@slidev/parser": "0.48.0-beta.15"
|
|
60
59
|
},
|
|
61
60
|
"devDependencies": {
|
|
62
61
|
"vite": "^5.1.4"
|
package/pages/notes.vue
CHANGED
|
@@ -52,6 +52,7 @@ function decreaseFontSize() {
|
|
|
52
52
|
:note-html="currentRoute?.meta?.slide?.noteHTML"
|
|
53
53
|
:placeholder="`No notes for Slide ${pageNo}.`"
|
|
54
54
|
:clicks-context="currentRoute?.meta?.__clicksContext"
|
|
55
|
+
:auto-scroll="true"
|
|
55
56
|
/>
|
|
56
57
|
</div>
|
|
57
58
|
<div class="flex-none border-t border-main">
|
package/pages/overview.vue
CHANGED
|
@@ -3,7 +3,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
3
3
|
import { useHead } from '@unhead/vue'
|
|
4
4
|
import type { RouteRecordRaw } from 'vue-router'
|
|
5
5
|
import type { ClicksContext } from 'packages/types'
|
|
6
|
-
import { themeVars } from '../env'
|
|
6
|
+
import { configs, themeVars } from '../env'
|
|
7
7
|
import { openInEditor, rawRoutes } from '../logic/nav'
|
|
8
8
|
import { useFixedClicks } from '../composables/useClicks'
|
|
9
9
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
@@ -12,14 +12,15 @@ import SlideContainer from '../internals/SlideContainer.vue'
|
|
|
12
12
|
import SlideWrapper from '../internals/SlideWrapper'
|
|
13
13
|
import DrawingPreview from '../internals/DrawingPreview.vue'
|
|
14
14
|
import IconButton from '../internals/IconButton.vue'
|
|
15
|
-
import
|
|
15
|
+
import NoteEditable from '../internals/NoteEditable.vue'
|
|
16
16
|
import OverviewClicksSlider from '../internals/OverviewClicksSlider.vue'
|
|
17
17
|
import { CLICKS_MAX } from '../constants'
|
|
18
18
|
|
|
19
19
|
const cardWidth = 450
|
|
20
20
|
|
|
21
|
+
const slideTitle = configs.titleTemplate.replace('%s', configs.title || 'Slidev')
|
|
21
22
|
useHead({
|
|
22
|
-
title:
|
|
23
|
+
title: `Overview - ${slideTitle}`,
|
|
23
24
|
})
|
|
24
25
|
|
|
25
26
|
const blocks: Map<number, HTMLElement> = reactive(new Map())
|
|
@@ -79,6 +80,15 @@ function scrollToSlide(idx: number) {
|
|
|
79
80
|
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
function onMarkerClick(e: MouseEvent, clicks: number, route: RouteRecordRaw) {
|
|
84
|
+
const ctx = getClicksContext(route)
|
|
85
|
+
if (ctx.current === clicks)
|
|
86
|
+
ctx.current = CLICKS_MAX
|
|
87
|
+
else
|
|
88
|
+
ctx.current = clicks
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
}
|
|
91
|
+
|
|
82
92
|
onMounted(() => {
|
|
83
93
|
nextTick(() => {
|
|
84
94
|
checkActiveBlocks()
|
|
@@ -191,7 +201,7 @@ onMounted(() => {
|
|
|
191
201
|
<carbon:pen />
|
|
192
202
|
</IconButton>
|
|
193
203
|
</div>
|
|
194
|
-
<
|
|
204
|
+
<NoteEditable
|
|
195
205
|
:no="idx"
|
|
196
206
|
class="max-w-250 w-250 text-lg rounded p3"
|
|
197
207
|
:auto-height="true"
|
|
@@ -199,6 +209,7 @@ onMounted(() => {
|
|
|
199
209
|
:clicks-context="getClicksContext(route)"
|
|
200
210
|
@dblclick="edittingNote !== idx ? edittingNote = idx : null"
|
|
201
211
|
@update:editing="edittingNote = null"
|
|
212
|
+
@marker-click="(e, clicks) => onMarkerClick(e, clicks, route)"
|
|
202
213
|
/>
|
|
203
214
|
<div
|
|
204
215
|
v-if="wordCounts[idx] > 0"
|
|
@@ -56,7 +56,10 @@ const slidesWithNote = computed(() => rawRoutes
|
|
|
56
56
|
<div class="flex-auto" />
|
|
57
57
|
</div>
|
|
58
58
|
</h2>
|
|
59
|
-
<NoteDisplay
|
|
59
|
+
<NoteDisplay
|
|
60
|
+
:note-html="slide!.noteHTML"
|
|
61
|
+
class="max-w-full"
|
|
62
|
+
/>
|
|
60
63
|
</div>
|
|
61
64
|
<hr v-if="index < slidesWithNote.length - 1" class="border-main mb-8">
|
|
62
65
|
</div>
|
package/pages/presenter.vue
CHANGED
|
@@ -14,8 +14,8 @@ import { useFixedClicks } from '../composables/useClicks'
|
|
|
14
14
|
import SlideWrapper from '../internals/SlideWrapper'
|
|
15
15
|
import SlideContainer from '../internals/SlideContainer.vue'
|
|
16
16
|
import NavControls from '../internals/NavControls.vue'
|
|
17
|
-
import
|
|
18
|
-
import
|
|
17
|
+
import QuickOverview from '../internals/QuickOverview.vue'
|
|
18
|
+
import NoteEditable from '../internals/NoteEditable.vue'
|
|
19
19
|
import NoteStatic from '../internals/NoteStatic.vue'
|
|
20
20
|
import Goto from '../internals/Goto.vue'
|
|
21
21
|
import SlidesShow from '../internals/SlidesShow.vue'
|
|
@@ -45,12 +45,19 @@ const nextFrame = computed(() => {
|
|
|
45
45
|
else
|
|
46
46
|
return null
|
|
47
47
|
})
|
|
48
|
+
|
|
48
49
|
const nextFrameClicksCtx = computed(() => {
|
|
49
50
|
return nextFrame.value && clicksCtxMap[+nextFrame.value[0].path - 1]
|
|
50
51
|
})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
|
|
53
|
+
watch(
|
|
54
|
+
[currentRoute, queryClicks],
|
|
55
|
+
() => {
|
|
56
|
+
if (nextFrameClicksCtx.value)
|
|
57
|
+
nextFrameClicksCtx.value.current = nextFrame.value![1]
|
|
58
|
+
},
|
|
59
|
+
{ immediate: true },
|
|
60
|
+
)
|
|
54
61
|
|
|
55
62
|
const SideEditor = shallowRef<any>()
|
|
56
63
|
if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
@@ -86,21 +93,6 @@ onMounted(() => {
|
|
|
86
93
|
<template>
|
|
87
94
|
<div class="bg-main h-full slidev-presenter">
|
|
88
95
|
<div class="grid-container" :class="`layout${presenterLayout}`">
|
|
89
|
-
<div class="grid-section top flex">
|
|
90
|
-
<img src="../assets/logo-title-horizontal.png" class="ml-2 my-auto h-10 py-1 lg:h-14 lg:py-2" style="height: 3.5rem;" alt="Slidev logo">
|
|
91
|
-
<div class="flex-auto" />
|
|
92
|
-
<div
|
|
93
|
-
class="timer-btn my-auto relative w-22px h-22px cursor-pointer text-lg"
|
|
94
|
-
opacity="50 hover:100"
|
|
95
|
-
@click="resetTimer"
|
|
96
|
-
>
|
|
97
|
-
<carbon:time class="absolute" />
|
|
98
|
-
<carbon:renew class="absolute opacity-0" />
|
|
99
|
-
</div>
|
|
100
|
-
<div class="text-2xl pl-2 pr-6 my-auto tabular-nums">
|
|
101
|
-
{{ timer }}
|
|
102
|
-
</div>
|
|
103
|
-
</div>
|
|
104
96
|
<div ref="main" class="relative grid-section main flex flex-col p-2 lg:p-4" :style="themeVars">
|
|
105
97
|
<SlideContainer
|
|
106
98
|
key="main"
|
|
@@ -110,8 +102,8 @@ onMounted(() => {
|
|
|
110
102
|
<SlidesShow render-context="presenter" />
|
|
111
103
|
</template>
|
|
112
104
|
</SlideContainer>
|
|
113
|
-
<div class="
|
|
114
|
-
|
|
105
|
+
<div class="absolute left-0 top-0 bg-main border-b border-r border-main px2 py1 op50 text-sm">
|
|
106
|
+
Current
|
|
115
107
|
</div>
|
|
116
108
|
</div>
|
|
117
109
|
<div class="relative grid-section next flex flex-col p-2 lg:p-4" :style="themeVars">
|
|
@@ -129,8 +121,8 @@ onMounted(() => {
|
|
|
129
121
|
render-context="previewNext"
|
|
130
122
|
/>
|
|
131
123
|
</SlideContainer>
|
|
132
|
-
<div class="
|
|
133
|
-
|
|
124
|
+
<div class="absolute left-0 top-0 bg-main border-b border-r border-main px2 py1 op50 text-sm">
|
|
125
|
+
Next
|
|
134
126
|
</div>
|
|
135
127
|
</div>
|
|
136
128
|
<!-- Notes -->
|
|
@@ -138,12 +130,12 @@ onMounted(() => {
|
|
|
138
130
|
<SideEditor />
|
|
139
131
|
</div>
|
|
140
132
|
<div v-else class="grid-section note grid grid-rows-[1fr_min-content] overflow-hidden">
|
|
141
|
-
<
|
|
133
|
+
<NoteEditable
|
|
142
134
|
v-if="__DEV__"
|
|
143
135
|
:key="`edit-${currentSlideId}`"
|
|
136
|
+
v-model:editing="notesEditing"
|
|
144
137
|
:no="currentSlideId"
|
|
145
138
|
class="w-full max-w-full h-full overflow-auto p-2 lg:p-4"
|
|
146
|
-
:editing="notesEditing"
|
|
147
139
|
:clicks-context="clicksContext"
|
|
148
140
|
:style="{ fontSize: `${presenterNotesFontSize}em` }"
|
|
149
141
|
/>
|
|
@@ -171,8 +163,20 @@ onMounted(() => {
|
|
|
171
163
|
</IconButton>
|
|
172
164
|
</div>
|
|
173
165
|
</div>
|
|
174
|
-
<div class="grid-section bottom">
|
|
166
|
+
<div class="grid-section bottom flex">
|
|
175
167
|
<NavControls :persist="true" />
|
|
168
|
+
<div flex-auto />
|
|
169
|
+
<div
|
|
170
|
+
class="timer-btn my-auto relative w-22px h-22px cursor-pointer text-lg"
|
|
171
|
+
opacity="50 hover:100"
|
|
172
|
+
@click="resetTimer"
|
|
173
|
+
>
|
|
174
|
+
<carbon:time class="absolute" />
|
|
175
|
+
<carbon:renew class="absolute opacity-0" />
|
|
176
|
+
</div>
|
|
177
|
+
<div class="text-2xl pl-2 pr-6 my-auto tabular-nums">
|
|
178
|
+
{{ timer }}
|
|
179
|
+
</div>
|
|
176
180
|
</div>
|
|
177
181
|
<DrawingControls v-if="__SLIDEV_FEATURE_DRAWINGS__" />
|
|
178
182
|
</div>
|
|
@@ -184,7 +188,7 @@ onMounted(() => {
|
|
|
184
188
|
</div>
|
|
185
189
|
</div>
|
|
186
190
|
<Goto />
|
|
187
|
-
<
|
|
191
|
+
<QuickOverview v-model="showOverview" />
|
|
188
192
|
</template>
|
|
189
193
|
|
|
190
194
|
<style scoped>
|
|
@@ -204,7 +208,7 @@ onMounted(() => {
|
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
.grid-container {
|
|
207
|
-
--uno: bg-
|
|
211
|
+
--uno: bg-gray/20;
|
|
208
212
|
height: 100%;
|
|
209
213
|
width: 100%;
|
|
210
214
|
display: grid;
|
|
@@ -213,9 +217,8 @@ onMounted(() => {
|
|
|
213
217
|
|
|
214
218
|
.grid-container.layout1 {
|
|
215
219
|
grid-template-columns: 1fr 1fr;
|
|
216
|
-
grid-template-rows:
|
|
220
|
+
grid-template-rows: 2fr 1fr min-content;
|
|
217
221
|
grid-template-areas:
|
|
218
|
-
'top top'
|
|
219
222
|
'main main'
|
|
220
223
|
'note next'
|
|
221
224
|
'bottom bottom';
|
|
@@ -223,9 +226,8 @@ onMounted(() => {
|
|
|
223
226
|
|
|
224
227
|
.grid-container.layout2 {
|
|
225
228
|
grid-template-columns: 3fr 2fr;
|
|
226
|
-
grid-template-rows:
|
|
229
|
+
grid-template-rows: 2fr 1fr min-content;
|
|
227
230
|
grid-template-areas:
|
|
228
|
-
'top top'
|
|
229
231
|
'note main'
|
|
230
232
|
'note next'
|
|
231
233
|
'bottom bottom';
|
|
@@ -234,9 +236,8 @@ onMounted(() => {
|
|
|
234
236
|
@media (max-aspect-ratio: 3/5) {
|
|
235
237
|
.grid-container.layout1 {
|
|
236
238
|
grid-template-columns: 1fr;
|
|
237
|
-
grid-template-rows:
|
|
239
|
+
grid-template-rows: 1fr 1fr 1fr min-content;
|
|
238
240
|
grid-template-areas:
|
|
239
|
-
'top'
|
|
240
241
|
'main'
|
|
241
242
|
'note'
|
|
242
243
|
'next'
|
|
@@ -247,9 +248,8 @@ onMounted(() => {
|
|
|
247
248
|
@media (min-aspect-ratio: 1/1) {
|
|
248
249
|
.grid-container.layout1 {
|
|
249
250
|
grid-template-columns: 1fr 1.1fr 0.9fr;
|
|
250
|
-
grid-template-rows:
|
|
251
|
+
grid-template-rows: 1fr 2fr min-content;
|
|
251
252
|
grid-template-areas:
|
|
252
|
-
'top top top'
|
|
253
253
|
'main main next'
|
|
254
254
|
'main main note'
|
|
255
255
|
'bottom bottom bottom';
|
|
@@ -279,10 +279,4 @@ onMounted(() => {
|
|
|
279
279
|
.grid-section.bottom {
|
|
280
280
|
grid-area: bottom;
|
|
281
281
|
}
|
|
282
|
-
.context {
|
|
283
|
-
position: absolute;
|
|
284
|
-
top: 0;
|
|
285
|
-
left: 0;
|
|
286
|
-
--uno: px-1 text-xs bg-gray-400 bg-opacity-50 opacity-75 rounded-br-md;
|
|
287
|
-
}
|
|
288
282
|
</style>
|
package/setup/root.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* __imports__ */
|
|
2
2
|
import { watch } from 'vue'
|
|
3
3
|
import { useHead } from '@unhead/vue'
|
|
4
|
-
import { nanoid } from 'nanoid'
|
|
5
4
|
import { configs } from '../env'
|
|
6
5
|
import { initSharedState, onPatch, patch } from '../state/shared'
|
|
7
6
|
import { initDrawingState } from '../state/drawings'
|
|
@@ -9,6 +8,7 @@ import { clicksContext, currentPage, getPath, isNotesViewer, isPresenter } from
|
|
|
9
8
|
import { router } from '../routes'
|
|
10
9
|
import { TRUST_ORIGINS } from '../constants'
|
|
11
10
|
import { skipTransition } from '../composables/hmr'
|
|
11
|
+
import { makeId } from '../logic/utils'
|
|
12
12
|
|
|
13
13
|
export default function setupRoot() {
|
|
14
14
|
// @ts-expect-error injected in runtime
|
|
@@ -25,7 +25,7 @@ export default function setupRoot() {
|
|
|
25
25
|
initSharedState(`${title} - shared`)
|
|
26
26
|
initDrawingState(`${title} - drawings`)
|
|
27
27
|
|
|
28
|
-
const id = `${location.origin}_${
|
|
28
|
+
const id = `${location.origin}_${makeId()}`
|
|
29
29
|
|
|
30
30
|
// update shared state
|
|
31
31
|
function updateSharedState() {
|