@slidev/client 0.39.0 → 0.40.1

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/constants.ts CHANGED
@@ -19,3 +19,8 @@ export const CLASS_VCLICK_GONE = 'slidev-vclick-gone'
19
19
  export const CLASS_VCLICK_HIDDEN_EXP = 'slidev-vclick-hidden-explicitly'
20
20
  export const CLASS_VCLICK_CURRENT = 'slidev-vclick-current'
21
21
  export const CLASS_VCLICK_PRIOR = 'slidev-vclick-prior'
22
+
23
+ export const TRUST_ORIGINS = [
24
+ 'localhost',
25
+ '127.0.0.1',
26
+ ]
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { ref } from 'vue'
3
- import { useDraggable, useStorage } from '@vueuse/core'
3
+ import { useDraggable, useLocalStorage } from '@vueuse/core'
4
4
 
5
5
  const props = defineProps<{
6
6
  storageKey?: string
@@ -10,7 +10,7 @@ const props = defineProps<{
10
10
  const el = ref<HTMLElement | null>(null)
11
11
  const initial = props.initial ?? { x: 0, y: 0 }
12
12
  const point = props.storageKey
13
- ? useStorage(props.storageKey, initial)
13
+ ? useLocalStorage(props.storageKey, initial)
14
14
  : ref(initial)
15
15
  const { style } = useDraggable(el, { initialValue: point })
16
16
  </script>
@@ -32,25 +32,25 @@ function setBrushColor(color: typeof brush.color) {
32
32
  :initial-x="10"
33
33
  :initial-y="10"
34
34
  >
35
- <button class="icon-btn" :class="{ shallow: drawingMode !== 'stylus' }" @click="setDrawingMode('stylus')">
35
+ <button class="slidev-icon-btn" :class="{ shallow: drawingMode !== 'stylus' }" @click="setDrawingMode('stylus')">
36
36
  <carbon:pen />
37
37
  </button>
38
- <button class="icon-btn" :class="{ shallow: drawingMode !== 'line' }" @click="setDrawingMode('line')">
38
+ <button class="slidev-icon-btn" :class="{ shallow: drawingMode !== 'line' }" @click="setDrawingMode('line')">
39
39
  <svg width="1em" height="1em" class="-mt-0.5" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
40
40
  <path d="M21.71 3.29a1 1 0 0 0-1.42 0l-18 18a1 1 0 0 0 0 1.42a1 1 0 0 0 1.42 0l18-18a1 1 0 0 0 0-1.42z" fill="currentColor" />
41
41
  </svg>
42
42
  </button>
43
- <button class="icon-btn" :class="{ shallow: drawingMode !== 'arrow' }" @click="setDrawingMode('arrow')">
43
+ <button class="slidev-icon-btn" :class="{ shallow: drawingMode !== 'arrow' }" @click="setDrawingMode('arrow')">
44
44
  <carbon:arrow-up-right />
45
45
  </button>
46
- <button class="icon-btn" :class="{ shallow: drawingMode !== 'ellipse' }" @click="setDrawingMode('ellipse')">
46
+ <button class="slidev-icon-btn" :class="{ shallow: drawingMode !== 'ellipse' }" @click="setDrawingMode('ellipse')">
47
47
  <carbon:radio-button />
48
48
  </button>
49
- <button class="icon-btn" :class="{ shallow: drawingMode !== 'rectangle' }" @click="setDrawingMode('rectangle')">
49
+ <button class="slidev-icon-btn" :class="{ shallow: drawingMode !== 'rectangle' }" @click="setDrawingMode('rectangle')">
50
50
  <carbon:checkbox />
51
51
  </button>
52
52
  <!-- TODO: not sure why it's not working! -->
53
- <!-- <button class="icon-btn" :class="{ shallow: drawingMode != 'eraseLine' }" @click="setDrawingMode('eraseLine')">
53
+ <!-- <button class="slidev-icon-btn" :class="{ shallow: drawingMode != 'eraseLine' }" @click="setDrawingMode('eraseLine')">
54
54
  <carbon:erase />
55
55
  </button> -->
56
56
 
@@ -59,7 +59,7 @@ function setBrushColor(color: typeof brush.color) {
59
59
  <button
60
60
  v-for="color of brushColors"
61
61
  :key="color"
62
- class="icon-btn"
62
+ class="slidev-icon-btn"
63
63
  :class="brush.color === color ? 'active' : 'shallow'"
64
64
  @click="setBrushColor(color)"
65
65
  >
@@ -72,24 +72,24 @@ function setBrushColor(color: typeof brush.color) {
72
72
 
73
73
  <VerticalDivider />
74
74
 
75
- <button class="icon-btn" :class="{ disabled: !canUndo }" @click="undo()">
75
+ <button class="slidev-icon-btn" :class="{ disabled: !canUndo }" @click="undo()">
76
76
  <carbon:undo />
77
77
  </button>
78
- <button class="icon-btn" :class="{ disabled: !canRedo }" @click="redo()">
78
+ <button class="slidev-icon-btn" :class="{ disabled: !canRedo }" @click="redo()">
79
79
  <carbon:redo />
80
80
  </button>
81
- <button class="icon-btn" :class="{ disabled: !canClear }" @click="clearDrauu()">
81
+ <button class="slidev-icon-btn" :class="{ disabled: !canClear }" @click="clearDrauu()">
82
82
  <carbon:delete />
83
83
  </button>
84
84
 
85
85
  <VerticalDivider />
86
- <button class="icon-btn" :class="{ shallow: !drawingPinned }" @click="drawingPinned = !drawingPinned">
86
+ <button class="slidev-icon-btn" :class="{ shallow: !drawingPinned }" @click="drawingPinned = !drawingPinned">
87
87
  <carbon:pin-filled v-show="drawingPinned" class="transform -rotate-45" />
88
88
  <carbon:pin v-show="!drawingPinned" />
89
89
  </button>
90
90
  <button
91
91
  v-if="drawingEnabled"
92
- class="icon-btn"
92
+ class="slidev-icon-btn"
93
93
  :class="{ shallow: !drawingEnabled }"
94
94
  @click="drawingEnabled = !drawingEnabled"
95
95
  >
@@ -137,10 +137,10 @@ throttledWatch(
137
137
  >
138
138
  <div class="flex pb-2 text-xl -mt-1">
139
139
  <div class="mr-4 rounded flex">
140
- <button class="icon-btn" :class="tab === 'content' ? 'text-$slidev-theme-primary' : ''" @click="switchTab('content')">
140
+ <button class="slidev-icon-btn" :class="tab === 'content' ? 'text-$slidev-theme-primary' : ''" @click="switchTab('content')">
141
141
  <carbon:account />
142
142
  </button>
143
- <button class="icon-btn" :class="tab === 'note' ? 'text-$slidev-theme-primary' : ''" @click="switchTab('note')">
143
+ <button class="slidev-icon-btn" :class="tab === 'note' ? 'text-$slidev-theme-primary' : ''" @click="switchTab('note')">
144
144
  <carbon:align-box-bottom-right />
145
145
  </button>
146
146
  </div>
@@ -148,10 +148,10 @@ throttledWatch(
148
148
  {{ tab === 'content' ? 'Slide' : 'Notes' }}
149
149
  </span>
150
150
  <div class="flex-auto" />
151
- <button class="icon-btn" @click="openInEditor()">
151
+ <button class="slidev-icon-btn" @click="openInEditor()">
152
152
  <carbon:launch />
153
153
  </button>
154
- <button class="icon-btn" @click="close">
154
+ <button class="slidev-icon-btn" @click="close">
155
155
  <carbon:close />
156
156
  </button>
157
157
  </div>
@@ -51,26 +51,26 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
51
51
  :class="barStyle"
52
52
  @mouseleave="onMouseLeave"
53
53
  >
54
- <button v-if="!isEmbedded" class="icon-btn" @click="toggleFullscreen">
54
+ <button v-if="!isEmbedded" class="slidev-icon-btn" @click="toggleFullscreen">
55
55
  <carbon:minimize v-if="isFullscreen" />
56
56
  <carbon:maximize v-else />
57
57
  </button>
58
58
 
59
- <button class="icon-btn" :class="{ disabled: !hasPrev }" @click="prev">
59
+ <button class="slidev-icon-btn" :class="{ disabled: !hasPrev }" @click="prev">
60
60
  <carbon:arrow-left />
61
61
  </button>
62
62
 
63
- <button class="icon-btn" :class="{ disabled: !hasNext }" title="Next" @click="next">
63
+ <button class="slidev-icon-btn" :class="{ disabled: !hasNext }" title="Next" @click="next">
64
64
  <carbon:arrow-right />
65
65
  </button>
66
66
 
67
- <button v-if="!isEmbedded" class="icon-btn" title="Slides overview" @click="toggleOverview()">
67
+ <button v-if="!isEmbedded" class="slidev-icon-btn" title="Slides overview" @click="toggleOverview()">
68
68
  <carbon:apps />
69
69
  </button>
70
70
 
71
71
  <button
72
72
  v-if="!isColorSchemaConfigured"
73
- class="icon-btn"
73
+ class="slidev-icon-btn"
74
74
  title="Toggle dark mode"
75
75
  @click="toggleDark()"
76
76
  >
@@ -88,7 +88,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
88
88
 
89
89
  <button
90
90
  v-if="isPresenter"
91
- class="icon-btn"
91
+ class="slidev-icon-btn"
92
92
  title="Show presenter cursor"
93
93
  @click="showPresenterCursor = !showPresenterCursor"
94
94
  >
@@ -98,7 +98,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
98
98
  </template>
99
99
 
100
100
  <template v-if="__SLIDEV_FEATURE_DRAWINGS__ && (!configs.drawings.presenterOnly || isPresenter) && !isEmbedded">
101
- <button class="icon-btn relative" title="Drawing" @click="drawingEnabled = !drawingEnabled">
101
+ <button class="slidev-icon-btn relative" title="Drawing" @click="drawingEnabled = !drawingEnabled">
102
102
  <carbon:pen />
103
103
  <div
104
104
  v-if="drawingEnabled"
@@ -110,26 +110,26 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
110
110
  </template>
111
111
 
112
112
  <template v-if="!isEmbedded">
113
- <RouterLink v-if="isPresenter" :to="nonPresenterLink" class="icon-btn" title="Play Mode">
113
+ <RouterLink v-if="isPresenter" :to="nonPresenterLink" class="slidev-icon-btn" title="Play Mode">
114
114
  <carbon:presentation-file />
115
115
  </RouterLink>
116
- <RouterLink v-if="__SLIDEV_FEATURE_PRESENTER__ && showPresenter" :to="presenterLink" class="icon-btn" title="Presenter Mode">
116
+ <RouterLink v-if="__SLIDEV_FEATURE_PRESENTER__ && showPresenter" :to="presenterLink" class="slidev-icon-btn" title="Presenter Mode">
117
117
  <carbon:user-speaker />
118
118
  </RouterLink>
119
119
 
120
- <button v-if="__DEV__ && !isPresenter" class="icon-btn <md:hidden" @click="showEditor = !showEditor">
120
+ <button v-if="__DEV__ && !isPresenter" class="slidev-icon-btn <md:hidden" @click="showEditor = !showEditor">
121
121
  <carbon:text-annotation-toggle />
122
122
  </button>
123
123
  </template>
124
124
  <template v-if="!__DEV__">
125
- <button v-if="configs.download" class="icon-btn" @click="downloadPDF">
125
+ <button v-if="configs.download" class="slidev-icon-btn" @click="downloadPDF">
126
126
  <carbon:download />
127
127
  </button>
128
128
  </template>
129
129
 
130
130
  <button
131
131
  v-if="!isPresenter && configs.info && !isEmbedded"
132
- class="icon-btn"
132
+ class="slidev-icon-btn"
133
133
  @click="showInfoDialog = !showInfoDialog"
134
134
  >
135
135
  <carbon:information />
@@ -138,7 +138,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
138
138
  <template v-if="!isPresenter && !isEmbedded">
139
139
  <MenuButton>
140
140
  <template #button>
141
- <button class="icon-btn">
141
+ <button class="slidev-icon-btn">
142
142
  <carbon:settings-adjust />
143
143
  </button>
144
144
  </template>
@@ -3,6 +3,7 @@ const props = defineProps<{
3
3
  class?: string
4
4
  noteHtml?: string
5
5
  note?: string
6
+ placeholder?: string
6
7
  }>()
7
8
 
8
9
  defineEmits(['click'])
@@ -17,10 +18,19 @@ defineEmits(['click'])
17
18
  v-html="noteHtml"
18
19
  />
19
20
  <div
20
- v-else
21
+ v-else-if="note"
21
22
  class="prose overflow-auto outline-none"
22
23
  :class="props.class"
23
24
  @click="$emit('click')"
24
- v-text="note"
25
- />
25
+ >
26
+ <p v-text="note" />
27
+ </div>
28
+ <div
29
+ v-else
30
+ class="prose overflow-auto outline-none opacity-50 italic"
31
+ :class="props.class"
32
+ @click="$emit('click')"
33
+ >
34
+ <p v-text="props.placeholder || 'No notes.'" />
35
+ </div>
26
36
  </template>
@@ -3,7 +3,7 @@ import { ignorableWatch, onClickOutside } from '@vueuse/core'
3
3
  import { nextTick, ref, watch } from 'vue'
4
4
  import { currentSlideId } from '../logic/nav'
5
5
  import { useDynamicSlideInfo } from '../logic/note'
6
- import NoteViewer from './NoteViewer.vue'
6
+ import NoteDisplay from './NoteDisplay.vue'
7
7
 
8
8
  const props = defineProps({
9
9
  class: {
@@ -60,11 +60,11 @@ onClickOutside(input, () => {
60
60
  </script>
61
61
 
62
62
  <template>
63
- <NoteViewer
63
+ <NoteDisplay
64
64
  v-if="!editing && note"
65
65
  :class="props.class"
66
66
  :note="note"
67
- :note-html="info?.notesHTML"
67
+ :note-html="info?.noteHTML"
68
68
  @click="switchNoteEdit"
69
69
  />
70
70
  <textarea
@@ -1,18 +1,18 @@
1
1
  <script setup lang="ts">
2
2
  import { computed } from 'vue'
3
3
  import { currentRoute } from '../logic/nav'
4
- import NoteViewer from './NoteViewer.vue'
4
+ import NoteDisplay from './NoteDisplay.vue'
5
5
 
6
6
  const props = defineProps<{
7
7
  class?: string
8
8
  }>()
9
9
 
10
10
  const note = computed(() => currentRoute.value?.meta?.slide?.note)
11
- const noteHtml = computed(() => currentRoute.value?.meta?.slide?.notesHTML)
11
+ const noteHtml = computed(() => currentRoute.value?.meta?.slide?.noteHTML)
12
12
  </script>
13
13
 
14
14
  <template>
15
- <NoteViewer
15
+ <NoteDisplay
16
16
  :class="props.class"
17
17
  :note="note"
18
18
  :note-html="noteHtml"
@@ -0,0 +1,76 @@
1
+ <script setup lang="ts">
2
+ import { useHead } from '@vueuse/head'
3
+ import { computed } from 'vue'
4
+ import { useLocalStorage } from '@vueuse/core'
5
+ import { configs } from '../env'
6
+ import { sharedState } from '../state/shared'
7
+ import { fullscreen } from '../state'
8
+ import { total } from '../logic/nav'
9
+ import { rawRoutes } from '../routes'
10
+ import NoteDisplay from './NoteDisplay.vue'
11
+
12
+ const slideTitle = configs.titleTemplate.replace('%s', configs.title || 'Slidev')
13
+ useHead({
14
+ title: `Notes - ${slideTitle}`,
15
+ })
16
+
17
+ const { isFullscreen, toggle: toggleFullscreen } = fullscreen
18
+
19
+ const fontSize = useLocalStorage('slidev-notes-font-size', 18)
20
+ const pageNo = computed(() => sharedState.lastUpdate?.type === 'viewer' ? sharedState.viewerPage : sharedState.page)
21
+ const currentRoute = computed(() => rawRoutes.find(i => i.path === `${pageNo.value}`))
22
+ const nextRoute = computed(() => rawRoutes.find(i => i.path === `${pageNo.value + 1}`))
23
+
24
+ function increaseFontSize() {
25
+ fontSize.value = fontSize.value + 1
26
+ }
27
+
28
+ function decreaseFontSize() {
29
+ fontSize.value = fontSize.value - 1
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div
35
+ class="fixed top-0 left-0 h-2px bg-teal-500 transition-all duration-500"
36
+ :style="{ width: `${(pageNo - 1) / total * 100}%` }"
37
+ />
38
+ <div class="h-full flex flex-col">
39
+ <div
40
+ class="px-5 flex-auto h-full overflow-auto"
41
+ :style="{ fontSize: `${fontSize}px` }"
42
+ >
43
+ <NoteDisplay
44
+ :note="currentRoute?.meta?.slide?.note"
45
+ :note-html="currentRoute?.meta?.slide?.noteHTML"
46
+ :placeholder="`No notes for Slide ${pageNo}.`"
47
+ />
48
+ </div>
49
+ <div v-if="nextRoute" class="px-5 py-2 max-h-60 overflow-auto border-t border-gray-400 border-opacity-20">
50
+ <NoteDisplay
51
+ class="opacity-50"
52
+ :note="nextRoute?.meta?.slide?.note"
53
+ :note-html="nextRoute?.meta?.slide?.noteHTML"
54
+ placeholder="No notes for next slide."
55
+ />
56
+ </div>
57
+ <div class="flex-none border-t border-gray-400 border-opacity-20">
58
+ <div class="flex gap-1 items-center px-6 py-3">
59
+ <button class="slidev-icon-btn" @click="toggleFullscreen">
60
+ <carbon:minimize v-if="isFullscreen" />
61
+ <carbon:maximize v-else />
62
+ </button>
63
+ <button class="slidev-icon-btn" @click="increaseFontSize">
64
+ <carbon:zoom-in />
65
+ </button>
66
+ <button class="slidev-icon-btn" @click="decreaseFontSize">
67
+ <carbon:zoom-out />
68
+ </button>
69
+ <div class="flex-auto" />
70
+ <div class="p2 text-center">
71
+ {{ pageNo }} / {{ total }}
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </template>
@@ -118,7 +118,7 @@ onMounted(() => {
118
118
  class="h-full w-full"
119
119
  >
120
120
  <SlideWrapper
121
- :is="nextSlide.route?.component"
121
+ :is="nextSlide.route?.component as any"
122
122
  v-model:clicks-elements="nextTabElements"
123
123
  :clicks="nextSlide.clicks"
124
124
  :clicks-disabled="false"
@@ -4,7 +4,7 @@ import { useStyleTag } from '@vueuse/core'
4
4
  import { useHead } from '@vueuse/head'
5
5
  import { configs, themeVars } from '../env'
6
6
  import { rawRoutes, total } from '../logic/nav'
7
- import NoteViewer from './NoteViewer.vue'
7
+ import NoteDisplay from './NoteDisplay.vue'
8
8
 
9
9
  useStyleTag(`
10
10
  @page {
@@ -29,7 +29,7 @@ useHead({ title: `Notes - ${configs.title}` })
29
29
  const slidesWithNote = computed(() => rawRoutes
30
30
  .slice(0, -1)
31
31
  .map(route => route.meta?.slide)
32
- .filter(slide => slide !== undefined && slide.notesHTML !== ''))
32
+ .filter(slide => slide !== undefined && slide.noteHTML !== ''))
33
33
  </script>
34
34
 
35
35
  <template>
@@ -55,7 +55,7 @@ const slidesWithNote = computed(() => rawRoutes
55
55
  <div class="flex-auto" />
56
56
  </div>
57
57
  </h2>
58
- <NoteViewer :note-html="slide!.notesHTML" class="max-w-full" />
58
+ <NoteDisplay :note-html="slide!.noteHTML" class="max-w-full" />
59
59
  </div>
60
60
  <hr v-if="index < slidesWithNote.length - 1" class="border-gray-400/50 mb-8">
61
61
  </div>
@@ -35,7 +35,7 @@ onMounted(() => {
35
35
  <template>
36
36
  <button
37
37
  v-if="currentCamera !== 'none'"
38
- class="icon-btn <md:hidden"
38
+ class="slidev-icon-btn <md:hidden"
39
39
  :class="{ 'text-green-500': Boolean(showAvatar && streamCamera) }"
40
40
  title="Show camera view"
41
41
  @click="toggleAvatar"
@@ -44,7 +44,7 @@ onMounted(() => {
44
44
  </button>
45
45
 
46
46
  <button
47
- class="icon-btn"
47
+ class="slidev-icon-btn"
48
48
  :class="{ 'text-red-500': recording }"
49
49
  title="Recording"
50
50
  @click="toggleRecording"
@@ -54,7 +54,7 @@ onMounted(() => {
54
54
  </button>
55
55
  <MenuButton :disabled="recording">
56
56
  <template #button>
57
- <button class="icon-btn h-full !text-sm !px-0">
57
+ <button class="slidev-icon-btn h-full !text-sm !px-0">
58
58
  <carbon:chevron-up class="opacity-50" />
59
59
  </button>
60
60
  </template>
@@ -101,7 +101,7 @@ watchEffect(() => {
101
101
  </div>
102
102
  </div>
103
103
  </div>
104
- <button v-if="value" class="fixed text-2xl top-4 right-4 icon-btn text-gray-400" @click="close">
104
+ <button v-if="value" class="fixed text-2xl top-4 right-4 slidev-icon-btn text-gray-400" @click="close">
105
105
  <carbon:close />
106
106
  </button>
107
107
  </template>
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { TransitionGroup, computed, shallowRef, watch } from 'vue'
2
+ import { computed, shallowRef, watch } from 'vue'
3
3
  import { clicks, currentRoute, isPresenter, nextRoute, rawRoutes, transition } from '../logic/nav'
4
4
  import { getSlideClass } from '../utils'
5
5
  import SlideWrapper from './SlideWrapper'
@@ -1,11 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { useDraggable, useEventListener, useStorage } from '@vueuse/core'
2
+ import { useDraggable, useEventListener, useLocalStorage } from '@vueuse/core'
3
3
  import { computed, onMounted, ref, watchEffect } from 'vue'
4
4
  import { currentCamera } from '../state'
5
5
  import { recorder } from '../logic/recording'
6
6
 
7
- const size = useStorage('slidev-webcam-size', Math.round(Math.min(window.innerHeight, (window.innerWidth) / 8)))
8
- const position = useStorage('slidev-webcam-pos', {
7
+ const size = useLocalStorage('slidev-webcam-size', Math.round(Math.min(window.innerHeight, (window.innerWidth) / 8)))
8
+ const position = useLocalStorage('slidev-webcam-pos', {
9
9
  x: window.innerWidth - size.value - 30,
10
10
  y: window.innerHeight - size.value - 30,
11
11
  }, undefined, { deep: true })
package/logic/dark.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { isClient, usePreferredDark, useStorage, useToggle } from '@vueuse/core'
1
+ import { isClient, useLocalStorage, usePreferredDark, useToggle } from '@vueuse/core'
2
2
  import { computed, watch } from 'vue'
3
3
  import { configs } from '../env'
4
4
 
5
5
  const preferredDark = usePreferredDark()
6
- const store = useStorage('slidev-color-schema', 'auto')
6
+ const store = useLocalStorage('slidev-color-schema', 'auto')
7
7
 
8
8
  export const isColorSchemaConfigured = computed(() => configs.colorSchema !== 'auto')
9
9
  export const isDark = computed<boolean>({
package/logic/drawings.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { computed, markRaw, nextTick, reactive, ref, watch } from 'vue'
2
2
  import type { Brush, Options as DrauuOptions, DrawingMode } from 'drauu'
3
3
  import { createDrauu } from 'drauu'
4
- import { toReactive, useStorage } from '@vueuse/core'
4
+ import { toReactive, useLocalStorage } from '@vueuse/core'
5
5
  import { drawingState, onPatch, patch } from '../state/drawings'
6
6
  import { configs } from '../env'
7
7
  import { currentPage, isPresenter } from './nav'
@@ -16,14 +16,14 @@ export const brushColors = [
16
16
  '#000000',
17
17
  ]
18
18
 
19
- export const drawingEnabled = useStorage('slidev-drawing-enabled', false)
20
- export const drawingPinned = useStorage('slidev-drawing-pinned', false)
19
+ export const drawingEnabled = useLocalStorage('slidev-drawing-enabled', false)
20
+ export const drawingPinned = useLocalStorage('slidev-drawing-pinned', false)
21
21
  export const canUndo = ref(false)
22
22
  export const canRedo = ref(false)
23
23
  export const canClear = ref(false)
24
24
  export const isDrawing = ref(false)
25
25
 
26
- export const brush = toReactive(useStorage<Brush>('slidev-drawing-brush', {
26
+ export const brush = toReactive(useLocalStorage<Brush>('slidev-drawing-brush', {
27
27
  color: brushColors[0],
28
28
  size: 4,
29
29
  mode: 'stylus',
package/logic/nav.ts CHANGED
@@ -26,6 +26,7 @@ export const isPrintMode = computed(() => route.value.query.print !== undefined)
26
26
  export const isPrintWithClicks = computed(() => route.value.query.print === 'clicks')
27
27
  export const isEmbedded = computed(() => route.value.query.embedded !== undefined)
28
28
  export const isPresenter = computed(() => route.value.path.startsWith('/presenter'))
29
+ export const isNotesViewer = computed(() => route.value.path.startsWith('/notes'))
29
30
  export const isClicksDisabled = computed(() => isPrintMode.value && !isPrintWithClicks.value)
30
31
  export const presenterPassword = computed(() => route.value.query.password)
31
32
  export const showPresenter = computed(() => !isPresenter.value && (!configs.remote || presenterPassword.value === configs.remote))
@@ -1,6 +1,6 @@
1
1
  import type { Ref } from 'vue'
2
2
  import { nextTick, ref, shallowRef, watch } from 'vue'
3
- import { useDevicesList, useEventListener, useStorage } from '@vueuse/core'
3
+ import { useDevicesList, useEventListener, useLocalStorage } from '@vueuse/core'
4
4
  import { isTruthy } from '@antfu/utils'
5
5
  import type RecorderType from 'recordrtc'
6
6
  import type { Options as RecorderOptions } from 'recordrtc'
@@ -11,7 +11,7 @@ type MimeType = Defined<RecorderOptions['mimeType']>
11
11
 
12
12
  export const recordingName = ref('')
13
13
  export const recordCamera = ref(true)
14
- export const mimeType = useStorage<MimeType>('slidev-record-mimetype', 'video/webm')
14
+ export const mimeType = useLocalStorage<MimeType>('slidev-record-mimetype', 'video/webm')
15
15
 
16
16
  export const mimeExtMap: Record<string, string> = {
17
17
  'video/webm': 'webm',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
- "version": "0.39.0",
3
+ "version": "0.40.1",
4
4
  "description": "Presentation slides for developers",
5
5
  "author": "antfu <anthonyfu117@hotmail.com>",
6
6
  "license": "MIT",
@@ -16,10 +16,10 @@
16
16
  },
17
17
  "dependencies": {
18
18
  "@antfu/utils": "^0.7.2",
19
- "@unocss/reset": "^0.49.1",
20
- "@vueuse/core": "^9.11.1",
21
- "@vueuse/head": "^1.0.23",
22
- "@vueuse/math": "^9.11.1",
19
+ "@unocss/reset": "^0.49.4",
20
+ "@vueuse/core": "^9.12.0",
21
+ "@vueuse/head": "^1.0.24",
22
+ "@vueuse/math": "^9.12.0",
23
23
  "@vueuse/motion": "^2.0.0-beta.27",
24
24
  "codemirror": "^5.65.5",
25
25
  "defu": "^6.1.2",
@@ -34,16 +34,16 @@
34
34
  "prettier": "^2.8.3",
35
35
  "recordrtc": "^5.6.2",
36
36
  "resolve": "^1.22.1",
37
- "unocss": "^0.49.1",
37
+ "unocss": "^0.49.4",
38
38
  "vite-plugin-windicss": "^1.8.10",
39
- "vue": "^3.2.45",
39
+ "vue": "^3.2.47",
40
40
  "vue-router": "^4.1.6",
41
41
  "vue-starport": "^0.3.0",
42
42
  "windicss": "^3.5.6",
43
- "@slidev/parser": "0.39.0",
44
- "@slidev/types": "0.39.0"
43
+ "@slidev/parser": "0.40.1",
44
+ "@slidev/types": "0.40.1"
45
45
  },
46
46
  "devDependencies": {
47
- "vite": "^4.0.4"
47
+ "vite": "^4.1.1"
48
48
  }
49
49
  }
package/routes.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { RouteRecordRaw } from 'vue-router'
2
- import type { SlideTransition } from '@slidev/types'
1
+ import type { RouteLocationNormalized, RouteRecordRaw } from 'vue-router'
3
2
  import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
3
+ import type { TransitionGroupProps } from 'vue'
4
4
  import Play from './internals/Play.vue'
5
5
  import Print from './internals/Print.vue'
6
6
  // @ts-expect-error missing types
@@ -25,24 +25,31 @@ export const routes: RouteRecordRaw[] = [
25
25
  ]
26
26
 
27
27
  if (__SLIDEV_FEATURE_PRESENTER__) {
28
+ function passwordGuard(to: RouteLocationNormalized) {
29
+ if (!_configs.remote || _configs.remote === to.query.password)
30
+ return true
31
+ if (_configs.remote && to.query.password === undefined) {
32
+ // eslint-disable-next-line no-alert
33
+ const password = prompt('Enter password')
34
+ if (_configs.remote === password)
35
+ return true
36
+ }
37
+ if (to.params.no)
38
+ return { path: `/${to.params.no}` }
39
+ return { path: '' }
40
+ }
28
41
  routes.push({ path: '/presenter/print', component: () => import('./internals/PresenterPrint.vue') })
42
+ routes.push({
43
+ name: 'notes',
44
+ path: '/notes',
45
+ component: () => import('./internals/NotesView.vue'),
46
+ beforeEnter: passwordGuard,
47
+ })
29
48
  routes.push({
30
49
  name: 'presenter',
31
50
  path: '/presenter/:no',
32
51
  component: () => import('./internals/Presenter.vue'),
33
- beforeEnter: (to) => {
34
- if (!_configs.remote || _configs.remote === to.query.password)
35
- return true
36
- if (_configs.remote && to.query.password === undefined) {
37
- // eslint-disable-next-line no-alert
38
- const password = prompt('Enter password')
39
- if (_configs.remote === password)
40
- return true
41
- }
42
- if (to.params.no)
43
- return { path: `/${to.params.no}` }
44
- return { path: '' }
45
- },
52
+ beforeEnter: passwordGuard,
46
53
  })
47
54
  routes.push({
48
55
  path: '/presenter',
@@ -65,14 +72,14 @@ declare module 'vue-router' {
65
72
  start: number
66
73
  end: number
67
74
  note?: string
68
- notesHTML?: string
75
+ noteHTML?: string
69
76
  id: number
70
77
  no: number
71
78
  filepath: string
72
79
  title?: string
73
80
  level?: number
74
81
  }
75
- transition?: string | SlideTransition
82
+ transition?: string | TransitionGroupProps | undefined
76
83
  // private fields
77
84
  __clicksElements: HTMLElement[]
78
85
  __preloaded?: boolean
package/setup/main.ts CHANGED
@@ -17,7 +17,7 @@ export default function setupMain(context: AppContext) {
17
17
  context.app.use(StarportPlugin({ keepAlive: true }))
18
18
 
19
19
  // @ts-expect-error inject in runtime
20
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ // eslint-disable-next-line unused-imports/no-unused-vars
21
21
  const injection_arg = context
22
22
 
23
23
  /* __injections__ */
package/setup/monaco.ts CHANGED
@@ -49,7 +49,7 @@ const setup = createSingletonPromise(async () => {
49
49
  ])
50
50
 
51
51
  // @ts-expect-error injected in runtime
52
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
52
+ // eslint-disable-next-line unused-imports/no-unused-vars
53
53
  const injection_arg = monaco
54
54
  // eslint-disable-next-line prefer-const
55
55
  let injection_return: MonacoSetupReturn = {}
package/setup/root.ts CHANGED
@@ -1,15 +1,17 @@
1
1
  /* __imports__ */
2
2
  import { watch } from 'vue'
3
3
  import { useHead, useHtmlAttrs } from '@vueuse/head'
4
+ import { nanoid } from 'nanoid'
4
5
  import { configs } from '../env'
5
6
  import { initSharedState, onPatch, patch } from '../state/shared'
6
7
  import { initDrawingState } from '../state/drawings'
7
- import { clicks, currentPage, getPath, isPresenter } from '../logic/nav'
8
+ import { clicks, currentPage, getPath, isNotesViewer, isPresenter } from '../logic/nav'
8
9
  import { router } from '../routes'
10
+ import { TRUST_ORIGINS } from '../constants'
9
11
 
10
12
  export default function setupRoot() {
11
13
  // @ts-expect-error injected in runtime
12
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+
13
15
  const injection_arg = undefined
14
16
 
15
17
  /* __injections__ */
@@ -20,17 +22,38 @@ export default function setupRoot() {
20
22
  initSharedState(`${title} - shared`)
21
23
  initDrawingState(`${title} - drawings`)
22
24
 
25
+ const id = `${location.origin}_${nanoid()}`
26
+
23
27
  // update shared state
24
28
  function updateSharedState() {
29
+ if (isNotesViewer.value)
30
+ return
31
+
32
+ // we allow Presenter mode, or Viewer mode from trusted origins to update the shared state
33
+ if (!isPresenter.value && !TRUST_ORIGINS.includes(location.host.split(':')[0]))
34
+ return
35
+
25
36
  if (isPresenter.value) {
26
37
  patch('page', +currentPage.value)
27
38
  patch('clicks', clicks.value)
28
39
  }
40
+ else {
41
+ patch('viewerPage', +currentPage.value)
42
+ patch('viewerClicks', clicks.value)
43
+ }
44
+ patch('lastUpdate', {
45
+ id,
46
+ type: isPresenter.value ? 'presenter' : 'viewer',
47
+ time: new Date().getTime(),
48
+ })
29
49
  }
30
50
  router.afterEach(updateSharedState)
31
51
  watch(clicks, updateSharedState)
32
52
 
33
53
  onPatch((state) => {
54
+ const routePath = router.currentRoute.value.path
55
+ if (!routePath.match(/^\/(\d+|presenter)\/?/))
56
+ return
34
57
  if (+state.page !== +currentPage.value || clicks.value !== state.clicks) {
35
58
  router.replace({
36
59
  path: getPath(state.page),
@@ -11,7 +11,7 @@ export default function setupShortcuts() {
11
11
  const { escape, space, shift, left, right, up, down, enter, d, g, o } = magicKeys
12
12
 
13
13
  // @ts-expect-error injected in runtime
14
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ // eslint-disable-next-line unused-imports/no-unused-vars
15
15
  const injection_arg: NavOperations = {
16
16
  next,
17
17
  prev,
package/state/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { breakpointsTailwind, isClient, useActiveElement, useBreakpoints, useFullscreen, useMagicKeys, useStorage, useToggle, useWindowSize } from '@vueuse/core'
1
+ import { breakpointsTailwind, isClient, useActiveElement, useBreakpoints, useFullscreen, useLocalStorage, useMagicKeys, useToggle, useWindowSize } from '@vueuse/core'
2
2
  import { computed, ref } from 'vue'
3
3
  import { slideAspect } from '../env'
4
4
 
@@ -20,13 +20,13 @@ export const activeElement = useActiveElement()
20
20
  export const isInputting = computed(() => ['INPUT', 'TEXTAREA'].includes(activeElement.value?.tagName || '') || activeElement.value?.classList.contains('CodeMirror-code'))
21
21
  export const isOnFocus = computed(() => ['BUTTON', 'A'].includes(activeElement.value?.tagName || ''))
22
22
 
23
- export const currentCamera = useStorage<string>('slidev-camera', 'default')
24
- export const currentMic = useStorage<string>('slidev-mic', 'default')
25
- export const slideScale = useStorage<number>('slidev-scale', 0)
23
+ export const currentCamera = useLocalStorage<string>('slidev-camera', 'default')
24
+ export const currentMic = useLocalStorage<string>('slidev-mic', 'default')
25
+ export const slideScale = useLocalStorage<number>('slidev-scale', 0)
26
26
 
27
- export const showOverview = useStorage('slidev-show-overview', false)
28
- export const showPresenterCursor = useStorage('slidev-presenter-cursor', true)
29
- export const showEditor = useStorage('slidev-show-editor', false)
30
- export const editorWidth = useStorage('slidev-editor-width', isClient ? window.innerWidth * 0.4 : 100)
27
+ export const showOverview = useLocalStorage('slidev-show-overview', false)
28
+ export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true)
29
+ export const showEditor = useLocalStorage('slidev-show-editor', false)
30
+ export const editorWidth = useLocalStorage('slidev-editor-width', isClient ? window.innerWidth * 0.4 : 100)
31
31
 
32
32
  export const toggleOverview = useToggle(showOverview)
package/state/shared.ts CHANGED
@@ -8,10 +8,27 @@ export interface SharedState {
8
8
  x: number
9
9
  y: number
10
10
  }
11
+
12
+ viewerPage: number
13
+ viewerClicks: number
14
+
15
+ lastUpdate?: {
16
+ id: string
17
+ type: 'presenter' | 'viewer'
18
+ time: number
19
+ }
11
20
  }
12
21
 
13
22
  const { init, onPatch, patch, state } = createSyncState<SharedState>(serverState, {
14
23
  page: 1,
15
24
  clicks: 0,
25
+ viewerPage: 1,
26
+ viewerClicks: 0,
16
27
  })
17
- export { init as initSharedState, onPatch, patch, state as sharedState }
28
+
29
+ export {
30
+ init as initSharedState,
31
+ onPatch,
32
+ patch,
33
+ state as sharedState,
34
+ }
package/styles/index.css CHANGED
@@ -15,22 +15,22 @@ html {
15
15
  background: transparent;
16
16
  }
17
17
 
18
- .icon-btn {
18
+ .slidev-icon-btn {
19
19
  @apply inline-block cursor-pointer select-none !outline-none;
20
20
  @apply opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
21
21
  @apply hover:(opacity-100 bg-gray-400 bg-opacity-10);
22
22
  @apply md:p-2;
23
23
  }
24
24
 
25
- .icon-btn.shallow {
25
+ .slidev-icon-btn.shallow {
26
26
  @apply opacity-30
27
27
  }
28
28
 
29
- .icon-btn.active {
29
+ .slidev-icon-btn.active {
30
30
  @apply opacity-100
31
31
  }
32
32
 
33
- .icon-btn.disabled {
33
+ .slidev-icon-btn.disabled {
34
34
  @apply opacity-25 pointer-events-none;
35
35
  }
36
36