@slidev/client 52.2.5 → 52.3.0
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/LightOrDark.vue +4 -2
- package/composables/useIME.ts +30 -0
- package/internals/NoteEditable.vue +1 -1
- package/internals/ShikiEditor.vue +6 -2
- package/logic/shortcuts.ts +2 -2
- package/package.json +10 -10
- package/pages/notes-edit.vue +83 -0
- package/pages/notes.vue +22 -1
- package/setup/monaco.ts +13 -0
- package/setup/routes.ts +6 -0
- package/state/storage.ts +23 -1
package/builtin/LightOrDark.vue
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ModelRef } from 'vue'
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
|
|
4
|
+
export function useIME(content: ModelRef<string>) {
|
|
5
|
+
const composingContent = ref(content.value)
|
|
6
|
+
watch(content, (v) => {
|
|
7
|
+
if (v !== composingContent.value) {
|
|
8
|
+
composingContent.value = v
|
|
9
|
+
}
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
function onInput(e: Event) {
|
|
13
|
+
if (!(e instanceof InputEvent) || !(e.target instanceof HTMLTextAreaElement)) {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (e.isComposing) {
|
|
18
|
+
composingContent.value = e.target.value
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
content.value = e.target.value
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function onCompositionEnd() {
|
|
26
|
+
content.value = composingContent.value
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { composingContent, onInput, onCompositionEnd }
|
|
30
|
+
}
|
|
@@ -131,7 +131,7 @@ watch(
|
|
|
131
131
|
v-else
|
|
132
132
|
ref="inputEl"
|
|
133
133
|
v-model="note"
|
|
134
|
-
class="prose resize-none overflow-auto outline-none bg-transparent block border-primary border-2"
|
|
134
|
+
class="prose dark:prose-invert resize-none overflow-auto outline-none bg-transparent block border-primary border-2"
|
|
135
135
|
style="line-height: 1.75;"
|
|
136
136
|
:style="[props.style, inputHeight != null ? { height: `${inputHeight}px` } : {}]"
|
|
137
137
|
:class="props.class"
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { getHighlighter } from '#slidev/shiki'
|
|
3
3
|
import { ref, shallowRef } from 'vue'
|
|
4
|
+
import { useIME } from '../composables/useIME'
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
placeholder?: string
|
|
7
8
|
}>()
|
|
8
9
|
const content = defineModel<string>({ required: true })
|
|
10
|
+
const { composingContent, onInput, onCompositionEnd } = useIME(content)
|
|
9
11
|
|
|
10
12
|
const textareaEl = ref<HTMLTextAreaElement | null>(null)
|
|
11
13
|
|
|
@@ -16,10 +18,12 @@ getHighlighter().then(h => highlight.value = h)
|
|
|
16
18
|
<template>
|
|
17
19
|
<div class="absolute left-3 right-0 inset-y-2 font-mono overflow-x-hidden overflow-y-auto cursor-text">
|
|
18
20
|
<div v-if="highlight" class="relative w-full h-max min-h-full">
|
|
19
|
-
<div class="relative w-full h-max" v-html="`${highlight(
|
|
21
|
+
<div class="relative w-full h-max" v-html="`${highlight(composingContent, 'markdown')} `" />
|
|
20
22
|
<textarea
|
|
21
|
-
ref="textareaEl" v-model="
|
|
23
|
+
ref="textareaEl" v-model="composingContent" :placeholder="props.placeholder"
|
|
22
24
|
class="absolute inset-0 resize-none text-transparent bg-transparent focus:outline-none caret-black dark:caret-white overflow-y-hidden"
|
|
25
|
+
@input="onInput"
|
|
26
|
+
@compositionend="onCompositionEnd"
|
|
23
27
|
/>
|
|
24
28
|
</div>
|
|
25
29
|
</div>
|
package/logic/shortcuts.ts
CHANGED
|
@@ -6,11 +6,11 @@ import { and, not } from '@vueuse/math'
|
|
|
6
6
|
import { watch } from 'vue'
|
|
7
7
|
import { useNav } from '../composables/useNav'
|
|
8
8
|
import setupShortcuts from '../setup/shortcuts'
|
|
9
|
-
import { fullscreen, isInputting, isOnFocus, magicKeys, shortcutsEnabled } from '../state'
|
|
9
|
+
import { fullscreen, isInputting, isOnFocus, magicKeys, shortcutsEnabled, shortcutsLocked } from '../state'
|
|
10
10
|
|
|
11
11
|
export function registerShortcuts() {
|
|
12
12
|
const { isPrintMode } = useNav()
|
|
13
|
-
const enabled = and(not(isInputting), not(isOnFocus), not(isPrintMode), shortcutsEnabled)
|
|
13
|
+
const enabled = and(not(isInputting), not(isOnFocus), not(isPrintMode), shortcutsEnabled, not(shortcutsLocked))
|
|
14
14
|
|
|
15
15
|
const allShortcuts = setupShortcuts()
|
|
16
16
|
const shortcuts = new Map<string | Ref<boolean>, ShortcutOptions>(
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "52.
|
|
4
|
+
"version": "52.3.0",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@antfu/utils": "^9.3.0",
|
|
32
|
-
"@iconify-json/carbon": "^1.2.
|
|
32
|
+
"@iconify-json/carbon": "^1.2.14",
|
|
33
33
|
"@iconify-json/ph": "^1.2.2",
|
|
34
34
|
"@iconify-json/svg-spinners": "^1.2.4",
|
|
35
35
|
"@shikijs/engine-javascript": "^3.13.0",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"@shikijs/vitepress-twoslash": "^3.13.0",
|
|
38
38
|
"@slidev/rough-notation": "^0.1.0",
|
|
39
39
|
"@typescript/ata": "^0.9.8",
|
|
40
|
-
"@unhead/vue": "^2.0.
|
|
41
|
-
"@unocss/reset": "^66.5.
|
|
40
|
+
"@unhead/vue": "^2.0.19",
|
|
41
|
+
"@unocss/reset": "^66.5.4",
|
|
42
42
|
"@vueuse/core": "^13.9.0",
|
|
43
43
|
"@vueuse/math": "^13.9.0",
|
|
44
44
|
"@vueuse/motion": "^3.0.3",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"file-saver": "^2.0.5",
|
|
47
47
|
"floating-vue": "^5.2.2",
|
|
48
48
|
"fuse.js": "^7.1.0",
|
|
49
|
-
"katex": "^0.16.
|
|
49
|
+
"katex": "^0.16.25",
|
|
50
50
|
"lz-string": "^1.5.0",
|
|
51
51
|
"mermaid": "^11.12.0",
|
|
52
52
|
"monaco-editor": "^0.53.0",
|
|
@@ -57,14 +57,14 @@
|
|
|
57
57
|
"shiki": "^3.13.0",
|
|
58
58
|
"shiki-magic-move": "^1.2.0",
|
|
59
59
|
"typescript": "^5.9.3",
|
|
60
|
-
"unocss": "^66.5.
|
|
60
|
+
"unocss": "^66.5.4",
|
|
61
61
|
"vue": "^3.5.22",
|
|
62
|
-
"vue-router": "^4.
|
|
62
|
+
"vue-router": "^4.6.3",
|
|
63
63
|
"yaml": "^2.8.1",
|
|
64
|
-
"@slidev/parser": "52.
|
|
65
|
-
"@slidev/types": "52.
|
|
64
|
+
"@slidev/parser": "52.3.0",
|
|
65
|
+
"@slidev/types": "52.3.0"
|
|
66
66
|
},
|
|
67
67
|
"devDependencies": {
|
|
68
|
-
"vite": "^7.1.
|
|
68
|
+
"vite": "^7.1.10"
|
|
69
69
|
}
|
|
70
70
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { SlideRoute } from '@slidev/types'
|
|
3
|
+
import { useHead } from '@unhead/vue'
|
|
4
|
+
import { debouncedWatch } from '@vueuse/core'
|
|
5
|
+
import { ref } from 'vue'
|
|
6
|
+
import { useNav } from '../composables/useNav'
|
|
7
|
+
import { useDynamicSlideInfo } from '../composables/useSlideInfo'
|
|
8
|
+
import { slidesTitle } from '../env'
|
|
9
|
+
import IconButton from '../internals/IconButton.vue'
|
|
10
|
+
import Modal from '../internals/Modal.vue'
|
|
11
|
+
|
|
12
|
+
useHead({ title: `Notes Edit - ${slidesTitle}` })
|
|
13
|
+
|
|
14
|
+
const { slides } = useNav()
|
|
15
|
+
|
|
16
|
+
const showHelp = ref(false)
|
|
17
|
+
const note = ref(serializeNotes(slides.value))
|
|
18
|
+
|
|
19
|
+
function serializeNotes(slides: SlideRoute[]) {
|
|
20
|
+
const lines: string[] = []
|
|
21
|
+
|
|
22
|
+
for (const slide of slides) {
|
|
23
|
+
if (!slide.meta.slide.note?.trim())
|
|
24
|
+
continue
|
|
25
|
+
lines.push(`--- #${slide.no}`)
|
|
26
|
+
lines.push('')
|
|
27
|
+
lines.push(slide.meta.slide.note)
|
|
28
|
+
lines.push('')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return lines.join('\n')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function deserializeNotes(notes: string, slides: SlideRoute[]) {
|
|
35
|
+
const lines = notes.split(/^(---\s*#\d+\s*)$/gm)
|
|
36
|
+
|
|
37
|
+
lines.forEach((line, index) => {
|
|
38
|
+
const match = line.match(/^---\s*#(\d+)\s*$/)
|
|
39
|
+
if (match) {
|
|
40
|
+
const no = Number.parseInt(match[1])
|
|
41
|
+
const note = lines[index + 1].trim()
|
|
42
|
+
const slide = slides.find(s => s.no === no)
|
|
43
|
+
if (slide) {
|
|
44
|
+
slide.meta.slide.note = note
|
|
45
|
+
useDynamicSlideInfo(no).update({ note })
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
debouncedWatch(note, (value) => {
|
|
52
|
+
deserializeNotes(value, slides.value)
|
|
53
|
+
}, { debounce: 300 })
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<Modal v-model="showHelp" class="px-6 py-4 flex flex-col gap-2">
|
|
58
|
+
<div class="flex gap-2 text-xl">
|
|
59
|
+
<div class="i-carbon:information my-auto" /> Help
|
|
60
|
+
</div>
|
|
61
|
+
<div class="prose dark:prose-invert">
|
|
62
|
+
<p>This is the batch notes editor. You can edit the notes for all the slides at once here.</p>
|
|
63
|
+
|
|
64
|
+
<p>The note for each slide are separated by <code>--- #[no]</code> lines, you might want to keep them while editing.</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="flex my-1">
|
|
67
|
+
<button class="slidev-form-button" @click="showHelp = false">
|
|
68
|
+
Close
|
|
69
|
+
</button>
|
|
70
|
+
</div>
|
|
71
|
+
</Modal>
|
|
72
|
+
<div class="h-full">
|
|
73
|
+
<div class="slidev-glass-effect fixed bottom-5 right-5 rounded-full border border-main">
|
|
74
|
+
<IconButton title="Help" class="rounded-full" @click="showHelp = true">
|
|
75
|
+
<div class="i-carbon:help text-2xl" />
|
|
76
|
+
</IconButton>
|
|
77
|
+
</div>
|
|
78
|
+
<textarea
|
|
79
|
+
v-model="note"
|
|
80
|
+
class="prose dark:prose-invert resize-none p5 outline-none bg-transparent block h-full w-full! max-w-full! max-h-full! min-h-full! min-w-full!"
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
package/pages/notes.vue
CHANGED
|
@@ -5,9 +5,9 @@ import { computed, ref, watch } from 'vue'
|
|
|
5
5
|
import { createClicksContextBase } from '../composables/useClicks'
|
|
6
6
|
import { useNav } from '../composables/useNav'
|
|
7
7
|
import { slidesTitle } from '../env'
|
|
8
|
-
|
|
9
8
|
import ClicksSlider from '../internals/ClicksSlider.vue'
|
|
10
9
|
import IconButton from '../internals/IconButton.vue'
|
|
10
|
+
import Modal from '../internals/Modal.vue'
|
|
11
11
|
import NoteDisplay from '../internals/NoteDisplay.vue'
|
|
12
12
|
import { fullscreen } from '../state'
|
|
13
13
|
import { sharedState } from '../state/shared'
|
|
@@ -20,6 +20,7 @@ const { isFullscreen, toggle: toggleFullscreen } = fullscreen
|
|
|
20
20
|
const scroller = ref<HTMLDivElement>()
|
|
21
21
|
const fontSize = useLocalStorage('slidev-notes-font-size', 18)
|
|
22
22
|
const pageNo = computed(() => sharedState.page)
|
|
23
|
+
const showHelp = ref(false)
|
|
23
24
|
const currentRoute = computed(() => slides.value.find(i => i.no === pageNo.value))
|
|
24
25
|
|
|
25
26
|
watch(pageNo, () => {
|
|
@@ -43,6 +44,20 @@ const clicksContext = computed(() => {
|
|
|
43
44
|
</script>
|
|
44
45
|
|
|
45
46
|
<template>
|
|
47
|
+
<Modal v-model="showHelp" class="px-6 py-4 flex flex-col gap-2">
|
|
48
|
+
<div class="flex gap-2 text-xl">
|
|
49
|
+
<div class="i-carbon:information my-auto" /> Help
|
|
50
|
+
</div>
|
|
51
|
+
<div class="prose dark:prose-invert">
|
|
52
|
+
<p>This is the hands-free live notes viewer.</p>
|
|
53
|
+
<p>It's designed to be used in a separate view or device. The progress is controlled by and auto synced with the main presenter or slide.</p>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="flex my-1">
|
|
56
|
+
<button class="slidev-form-button" @click="showHelp = false">
|
|
57
|
+
Close
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</Modal>
|
|
46
61
|
<div
|
|
47
62
|
class="fixed top-0 left-0 h-3px bg-primary transition-all duration-500"
|
|
48
63
|
:style="{ width: `${(pageNo - 1) / (total - 1) * 100 + 1}%` }"
|
|
@@ -76,6 +91,12 @@ const clicksContext = computed(() => {
|
|
|
76
91
|
<IconButton title="Decrease font size" @click="decreaseFontSize">
|
|
77
92
|
<div class="i-carbon:zoom-out" />
|
|
78
93
|
</IconButton>
|
|
94
|
+
<IconButton title="Edit notes" to="/notes-edit" target="_blank">
|
|
95
|
+
<div class="i-carbon:edit" />
|
|
96
|
+
</IconButton>
|
|
97
|
+
<IconButton title="Help" class="rounded-full" @click="showHelp = true">
|
|
98
|
+
<div class="i-carbon:help" />
|
|
99
|
+
</IconButton>
|
|
79
100
|
<div class="flex-auto" />
|
|
80
101
|
<div class="p2 text-center">
|
|
81
102
|
{{ pageNo }} / {{ total }}
|
package/setup/monaco.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { SyncDescriptor } from 'monaco-editor/esm/vs/platform/instantiation/comm
|
|
|
20
20
|
import ts from 'typescript'
|
|
21
21
|
import { watchEffect } from 'vue'
|
|
22
22
|
import { isDark } from '../logic/dark'
|
|
23
|
+
import { lockShortcuts } from '../state'
|
|
23
24
|
|
|
24
25
|
window.MonacoEnvironment = {
|
|
25
26
|
getWorker(_, label) {
|
|
@@ -97,6 +98,18 @@ const setup = createSingletonPromise(async () => {
|
|
|
97
98
|
Object.assign(editorOptions, result?.editorOptions)
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// Disable shortcuts when focusing Monaco editor.
|
|
102
|
+
monaco.editor.onDidCreateEditor((editor) => {
|
|
103
|
+
let release: (() => void) | null = null
|
|
104
|
+
editor.onDidFocusEditorWidget(() => {
|
|
105
|
+
release = lockShortcuts()
|
|
106
|
+
})
|
|
107
|
+
editor.onDidBlurEditorWidget(() => {
|
|
108
|
+
release?.()
|
|
109
|
+
release = null
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
100
113
|
// Use Shiki to highlight Monaco
|
|
101
114
|
shikiToMonaco(highlighter, monaco)
|
|
102
115
|
if (typeof themes === 'string') {
|
package/setup/routes.ts
CHANGED
|
@@ -39,6 +39,12 @@ export default function setupRoutes() {
|
|
|
39
39
|
component: () => import('../pages/notes.vue'),
|
|
40
40
|
beforeEnter: passwordGuard,
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
name: 'notes-edit',
|
|
44
|
+
path: '/notes-edit',
|
|
45
|
+
component: () => import('../pages/notes-edit.vue'),
|
|
46
|
+
beforeEnter: passwordGuard,
|
|
47
|
+
},
|
|
42
48
|
{
|
|
43
49
|
name: 'presenter',
|
|
44
50
|
path: '/presenter/:no',
|
package/state/storage.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { DragElementState } from '../composables/useDragElements'
|
|
2
2
|
import { breakpointsTailwind, isClient, useActiveElement, useBreakpoints, useFullscreen, useLocalStorage, useMagicKeys, useToggle, useWindowSize } from '@vueuse/core'
|
|
3
|
-
import { computed, ref, shallowRef } from 'vue'
|
|
3
|
+
import { computed, reactive, ref, shallowRef } from 'vue'
|
|
4
4
|
import { slideAspect } from '../env'
|
|
5
5
|
|
|
6
6
|
export const showRecordingDialog = ref(false)
|
|
@@ -16,6 +16,28 @@ export const hmrSkipTransition = ref(false)
|
|
|
16
16
|
export const disableTransition = ref(false)
|
|
17
17
|
|
|
18
18
|
export const shortcutsEnabled = ref(true)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether the keyboard shortcuts are enabled. Readonly,
|
|
22
|
+
* use `lockShortcuts` and `releaseShortcuts` to modify.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
// Use a locking mechanism to support multiple simultaneous locks
|
|
26
|
+
// and avoid race conditions. Race conditions may occur, for example,
|
|
27
|
+
// when locking shortcuts on editor focus and moving from one editor
|
|
28
|
+
// to another, as blur events can be triggered after focus.
|
|
29
|
+
const shortcutsLocks = reactive(new Set<symbol>())
|
|
30
|
+
|
|
31
|
+
export const shortcutsLocked = computed(() => shortcutsLocks.size > 0)
|
|
32
|
+
|
|
33
|
+
export function lockShortcuts() {
|
|
34
|
+
const lock = Symbol('shortcuts lock')
|
|
35
|
+
shortcutsLocks.add(lock)
|
|
36
|
+
return () => {
|
|
37
|
+
shortcutsLocks.delete(lock)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
19
41
|
export const breakpoints = useBreakpoints({
|
|
20
42
|
xs: 460,
|
|
21
43
|
...breakpointsTailwind,
|