@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.
@@ -3,6 +3,8 @@ import { isDark } from '../logic/dark'
3
3
  </script>
4
4
 
5
5
  <template>
6
- <slot v-if="isDark" name="dark" />
7
- <slot v-else name="light" />
6
+ <div>
7
+ <slot v-if="isDark" name="dark" />
8
+ <slot v-else name="light" />
9
+ </div>
8
10
  </template>
@@ -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(content, 'markdown')}&nbsp;`" />
21
+ <div class="relative w-full h-max" v-html="`${highlight(composingContent, 'markdown')}&nbsp;`" />
20
22
  <textarea
21
- ref="textareaEl" v-model="content" :placeholder="props.placeholder"
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>
@@ -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.2.5",
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.13",
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.17",
41
- "@unocss/reset": "^66.5.2",
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.23",
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.2",
60
+ "unocss": "^66.5.4",
61
61
  "vue": "^3.5.22",
62
- "vue-router": "^4.5.1",
62
+ "vue-router": "^4.6.3",
63
63
  "yaml": "^2.8.1",
64
- "@slidev/parser": "52.2.5",
65
- "@slidev/types": "52.2.5"
64
+ "@slidev/parser": "52.3.0",
65
+ "@slidev/types": "52.3.0"
66
66
  },
67
67
  "devDependencies": {
68
- "vite": "^7.1.9"
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,