@slidev/client 52.2.4 → 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/builtin/ShikiMagicMove.vue +38 -1
- package/composables/useIME.ts +30 -0
- package/internals/NoteDisplay.vue +3 -3
- package/internals/NoteEditable.vue +1 -1
- package/internals/ShikiEditor.vue +6 -2
- package/logic/shortcuts.ts +2 -2
- package/package.json +14 -14
- 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/styles/index.css +5 -1
package/builtin/LightOrDark.vue
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { KeyedTokensInfo } from 'shiki-magic-move/types'
|
|
3
3
|
import type { PropType } from 'vue'
|
|
4
4
|
import { sleep } from '@antfu/utils'
|
|
5
|
+
import { useClipboard } from '@vueuse/core'
|
|
5
6
|
import lz from 'lz-string'
|
|
6
7
|
import { ShikiMagicMovePrecompiled } from 'shiki-magic-move/vue'
|
|
7
8
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
@@ -43,6 +44,33 @@ const id = makeId()
|
|
|
43
44
|
const stepIndex = ref(0)
|
|
44
45
|
const container = ref<HTMLElement>()
|
|
45
46
|
|
|
47
|
+
const showCopyButton = computed(() => {
|
|
48
|
+
if (!configs.codeCopy)
|
|
49
|
+
return false
|
|
50
|
+
|
|
51
|
+
const magicCopy = configs.magicMoveCopy
|
|
52
|
+
if (!magicCopy)
|
|
53
|
+
return false
|
|
54
|
+
|
|
55
|
+
if (magicCopy === true || magicCopy === 'always')
|
|
56
|
+
return true
|
|
57
|
+
|
|
58
|
+
if (magicCopy === 'final')
|
|
59
|
+
return stepIndex.value === steps.length - 1
|
|
60
|
+
|
|
61
|
+
return false
|
|
62
|
+
})
|
|
63
|
+
const { copied, copy } = useClipboard()
|
|
64
|
+
|
|
65
|
+
function copyCode() {
|
|
66
|
+
// Use the code property directly from KeyedTokensInfo
|
|
67
|
+
const currentStep = steps[stepIndex.value]
|
|
68
|
+
if (!currentStep || !currentStep.code)
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
copy(currentStep.code.trim())
|
|
72
|
+
}
|
|
73
|
+
|
|
46
74
|
// Normalized the ranges, to at least have one range
|
|
47
75
|
const ranges = computed(() => props.stepRanges.map(i => i.length ? i : ['all']))
|
|
48
76
|
|
|
@@ -116,7 +144,7 @@ onMounted(() => {
|
|
|
116
144
|
</script>
|
|
117
145
|
|
|
118
146
|
<template>
|
|
119
|
-
<div ref="container" class="slidev-code-wrapper slidev-code-magic-move relative">
|
|
147
|
+
<div ref="container" class="slidev-code-wrapper slidev-code-magic-move relative group">
|
|
120
148
|
<div v-if="title" class="slidev-code-block-title">
|
|
121
149
|
<TitleIcon :title="title" />
|
|
122
150
|
<div class="leading-1em">
|
|
@@ -135,6 +163,15 @@ onMounted(() => {
|
|
|
135
163
|
stagger: 1,
|
|
136
164
|
}"
|
|
137
165
|
/>
|
|
166
|
+
<button
|
|
167
|
+
v-if="showCopyButton"
|
|
168
|
+
class="slidev-code-copy absolute right-0 transition opacity-0 group-hover:opacity-20 hover:!opacity-100"
|
|
169
|
+
:class="title ? 'top-10' : 'top-0'"
|
|
170
|
+
:title="copied ? 'Copied' : 'Copy'" @click="copyCode()"
|
|
171
|
+
>
|
|
172
|
+
<ph-check-circle v-if="copied" class="p-2 w-8 h-8" />
|
|
173
|
+
<ph-clipboard v-else class="p-2 w-8 h-8" />
|
|
174
|
+
</button>
|
|
138
175
|
</div>
|
|
139
176
|
</template>
|
|
140
177
|
|
|
@@ -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
|
+
}
|
|
@@ -148,20 +148,20 @@ watchEffect(() => {
|
|
|
148
148
|
<div
|
|
149
149
|
v-if="noteHtml"
|
|
150
150
|
ref="noteDisplay"
|
|
151
|
-
class="prose overflow-auto outline-none slidev-note"
|
|
151
|
+
class="prose dark:prose-invert overflow-auto outline-none slidev-note"
|
|
152
152
|
:class="[props.class, withClicks ? 'slidev-note-with-clicks' : '']"
|
|
153
153
|
v-html="noteHtml"
|
|
154
154
|
/>
|
|
155
155
|
<div
|
|
156
156
|
v-else-if="note"
|
|
157
|
-
class="prose overflow-auto outline-none slidev-note"
|
|
157
|
+
class="prose dark:prose-invert overflow-auto outline-none slidev-note"
|
|
158
158
|
:class="props.class"
|
|
159
159
|
>
|
|
160
160
|
<p v-text="note" />
|
|
161
161
|
</div>
|
|
162
162
|
<div
|
|
163
163
|
v-else
|
|
164
|
-
class="prose overflow-auto outline-none opacity-50 italic select-none slidev-note"
|
|
164
|
+
class="prose dark:prose-invert overflow-auto outline-none opacity-50 italic select-none slidev-note"
|
|
165
165
|
:class="props.class"
|
|
166
166
|
>
|
|
167
167
|
<p v-text="props.placeholder || 'No notes.'" />
|
|
@@ -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",
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"node": ">=18.0.0"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@antfu/utils": "^9.
|
|
32
|
-
"@iconify-json/carbon": "^1.2.
|
|
31
|
+
"@antfu/utils": "^9.3.0",
|
|
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",
|
|
@@ -55,16 +55,16 @@
|
|
|
55
55
|
"prettier": "^3.6.2",
|
|
56
56
|
"recordrtc": "^5.6.2",
|
|
57
57
|
"shiki": "^3.13.0",
|
|
58
|
-
"shiki-magic-move": "^1.
|
|
59
|
-
"typescript": "^5.9.
|
|
60
|
-
"unocss": "^66.5.
|
|
61
|
-
"vue": "^3.5.
|
|
62
|
-
"vue-router": "^4.
|
|
58
|
+
"shiki-magic-move": "^1.2.0",
|
|
59
|
+
"typescript": "^5.9.3",
|
|
60
|
+
"unocss": "^66.5.4",
|
|
61
|
+
"vue": "^3.5.22",
|
|
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,
|
package/styles/index.css
CHANGED