@slidev/client 0.30.2 → 0.31.2

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 CHANGED
@@ -11,8 +11,6 @@ Simple Arrow
11
11
  <script setup lang="ts">
12
12
  import { customAlphabet } from 'nanoid'
13
13
 
14
- const nanoid = customAlphabet('abcedfghijklmn', 10)
15
-
16
14
  defineProps<{
17
15
  x1: number | string
18
16
  y1: number | string
@@ -22,6 +20,8 @@ defineProps<{
22
20
  color?: string
23
21
  }>()
24
22
 
23
+ const nanoid = customAlphabet('abcedfghijklmn', 10)
24
+
25
25
  const id = nanoid()
26
26
  </script>
27
27
 
@@ -18,9 +18,6 @@ or
18
18
  import { useElementSize, useVModel } from '@vueuse/core'
19
19
  import { computed, ref, watch } from 'vue'
20
20
 
21
- const emit = defineEmits<{
22
- (e: any): void
23
- }>()
24
21
  const props = defineProps({
25
22
  modelValue: {
26
23
  default: '',
@@ -33,6 +30,9 @@ const props = defineProps({
33
30
  },
34
31
  })
35
32
 
33
+ const emit = defineEmits<{
34
+ (e: any): void
35
+ }>()
36
36
  const container = ref<HTMLDivElement>()
37
37
  const inner = ref<HTMLDivElement>()
38
38
  const size = ref(100)
@@ -45,7 +45,7 @@ const innerSize = useElementSize(inner)
45
45
  const wrapLen = ref(0)
46
46
  const wrap = ref('nowrap')
47
47
 
48
- watch([container, value, containerSize.width, innerSize.width], async() => {
48
+ watch([container, value, containerSize.width, innerSize.width], async () => {
49
49
  if (!container.value || innerSize.width.value <= 0)
50
50
  return
51
51
  const ratio = containerSize.width.value / innerSize.width.value
package/builtin/Link.vue CHANGED
@@ -10,6 +10,8 @@ Usage:
10
10
  <script setup lang="ts">
11
11
  import { isPrintMode } from '../logic/nav'
12
12
 
13
+ /* eslint-disable vue/no-v-text-v-html-on-component */
14
+
13
15
  defineProps<{
14
16
  to: number | string
15
17
  title?: string
@@ -21,6 +23,6 @@ defineProps<{
21
23
  <RouterLink v-else-if="!isPrintMode && !title" :to="to" @click="$event.target.blur()">
22
24
  <slot />
23
25
  </RouterLink>
24
- <a v-else-if="isPrintMode && title" :href="'#' + to" v-html="title" />
25
- <a v-else :href="'#' + to"><slot /></a>
26
+ <a v-else-if="isPrintMode && title" :href="`#${to}`" v-html="title" />
27
+ <a v-else :href="`#${to}`"><slot /></a>
26
28
  </template>
@@ -11,7 +11,6 @@ Alice -> Bob : Hello!
11
11
  ```
12
12
  -->
13
13
  <script setup lang="ts">
14
-
15
14
  import { computed } from 'vue'
16
15
 
17
16
  const props = defineProps<{
@@ -21,9 +20,8 @@ const props = defineProps<{
21
20
  }>()
22
21
 
23
22
  const uri = computed(() => `${props.server}/svg/${props.code}`)
24
-
25
23
  </script>
26
24
 
27
25
  <template>
28
- <img alt="PlantUML diagram" :src="uri" :style="{scale}">
26
+ <img alt="PlantUML diagram" :src="uri" :style="{ scale }">
29
27
  </template>
package/builtin/Toc.vue CHANGED
@@ -12,8 +12,6 @@ import { computed, inject } from 'vue'
12
12
  import type { TocItem } from '../logic/nav'
13
13
  import { injectionSlidevContext } from '../constants'
14
14
 
15
- const $slidev = inject(injectionSlidevContext)
16
-
17
15
  const props = withDefaults(
18
16
  defineProps<{
19
17
  columns?: string | number
@@ -31,6 +29,8 @@ const props = withDefaults(
31
29
  },
32
30
  )
33
31
 
32
+ const $slidev = inject(injectionSlidevContext)
33
+
34
34
  function filterTreeDepth(tree: TocItem[], level = 1): TocItem[] {
35
35
  if (level > Number(props.maxDepth)) {
36
36
  return []
@@ -30,11 +30,20 @@ const classes = computed(() => {
30
30
 
31
31
  <template>
32
32
  <ol v-if="list && list.length > 0" :class="classes">
33
- <li v-for="item in list" :key="item.path" :class="['slidev-toc-item', {'slidev-toc-item-active': item.active}, {'slidev-toc-item-parent-active': item.activeParent}]">
33
+ <li
34
+ v-for="item in list"
35
+ :key="item.path" class="slidev-toc-item"
36
+ :class="[{ 'slidev-toc-item-active': item.active }, { 'slidev-toc-item-parent-active': item.activeParent }]"
37
+ >
34
38
  <Link :to="item.path">
35
39
  <Titles :no="item.path" />
36
40
  </Link>
37
- <TocList v-if="item.children.length > 0" :level="level + 1" :list="item.children" :list-class="listClass" />
41
+ <TocList
42
+ v-if="item.children.length > 0"
43
+ :level="level + 1"
44
+ :list="item.children"
45
+ :list-class="listClass"
46
+ />
38
47
  </li>
39
48
  </ol>
40
49
  </template>
package/builtin/Tweet.vue CHANGED
@@ -21,10 +21,11 @@ const tweet = ref<HTMLElement | null>()
21
21
 
22
22
  const vm = getCurrentInstance()!
23
23
  const loaded = ref(false)
24
+ const tweetNotFound = ref(false)
24
25
 
25
26
  async function create() {
26
27
  // @ts-expect-error global
27
- await window.twttr.widgets.createTweet(
28
+ const element = await window.twttr.widgets.createTweet(
28
29
  props.id.toString(),
29
30
  tweet.value,
30
31
  {
@@ -33,6 +34,8 @@ async function create() {
33
34
  },
34
35
  )
35
36
  loaded.value = true
37
+ if (element === undefined)
38
+ tweetNotFound.value = true
36
39
  }
37
40
 
38
41
  // @ts-expect-error global
@@ -55,10 +58,11 @@ else {
55
58
 
56
59
  <template>
57
60
  <Transform :scale="scale || 1">
58
- <div ref="tweet" class="tweet" data-waitfor="iframe">
59
- <div v-if="!loaded" class="w-30 h-30 my-10px bg-gray-400 bg-opacity-10 rounded-lg flex opacity-50">
61
+ <div ref="tweet" class="tweet" :data-waitfor="tweetNotFound ? '' : 'iframe'">
62
+ <div v-if="!loaded || tweetNotFound" class="w-30 h-30 my-10px bg-gray-400 bg-opacity-10 rounded-lg flex opacity-50">
60
63
  <div class="m-auto animate-pulse text-4xl">
61
64
  <carbon:logo-twitter />
65
+ <span v-if="tweetNotFound">Could not load tweet with id="{{ props.id }}"</span>
62
66
  </div>
63
67
  </div>
64
68
  </div>
@@ -11,7 +11,7 @@ export function useNavClicks(
11
11
  // force update collected elements when the route is fully resolved
12
12
  const routeForceRefresh = ref(0)
13
13
  nextTick(() => {
14
- router.afterEach(async() => {
14
+ router.afterEach(async () => {
15
15
  await nextTick()
16
16
  routeForceRefresh.value += 1
17
17
  })
package/env.ts CHANGED
@@ -3,23 +3,8 @@ import { computed } from 'vue'
3
3
  import { objectMap } from '@antfu/utils'
4
4
  // @ts-expect-error missing types
5
5
  import _configs from '/@slidev/configs'
6
- import _serverState from 'server-reactive:nav'
7
- import _serverDrawingState from 'server-reactive:drawings?diff'
8
- import type { ServerReactive } from 'vite-plugin-vue-server-ref'
9
6
 
10
- export interface ServerState {
11
- page: number
12
- clicks: number
13
- cursor?: {
14
- x: number
15
- y: number
16
- }
17
- }
18
-
19
- export const serverState = _serverState as ServerReactive<ServerState>
20
- export const serverDrawingState = _serverDrawingState as ServerReactive<Record<number, string | undefined>>
21
7
  export const configs = _configs as SlidevConfig
22
-
23
8
  export const slideAspect = configs.aspectRatio ?? (16 / 9)
24
9
  export const slideWidth = configs.canvasWidth ?? 980
25
10
  export const slideHeight = Math.round(slideWidth / slideAspect)
@@ -8,7 +8,7 @@ import Goto from './Goto.vue'
8
8
 
9
9
  const WebCamera = shallowRef<any>()
10
10
  const RecordingDialog = shallowRef<any>()
11
- if (__DEV__) {
11
+ if (__SLIDEV_FEATURE_RECORD__) {
12
12
  import('./WebCamera.vue').then(v => WebCamera.value = v.default)
13
13
  import('./RecordingDialog.vue').then(v => RecordingDialog.value = v.default)
14
14
  }
@@ -17,9 +17,7 @@ if (__DEV__) {
17
17
  <template>
18
18
  <SlidesOverview v-model="showOverview" />
19
19
  <Goto />
20
- <template v-if="__DEV__">
21
- <WebCamera v-if="WebCamera" />
22
- <RecordingDialog v-if="RecordingDialog" v-model="showRecordingDialog" />
23
- </template>
20
+ <WebCamera v-if="WebCamera" />
21
+ <RecordingDialog v-if="RecordingDialog" v-model="showRecordingDialog" />
24
22
  <InfoDialog v-if="configs.info" v-model="showInfoDialog" />
25
23
  </template>
@@ -31,21 +31,21 @@ function setBrushColor(color: typeof brush.color) {
31
31
  :initial-x="10"
32
32
  :initial-y="10"
33
33
  >
34
- <button class="icon-btn" :class="{ shallow: drawingMode != 'stylus' }" @click="setDrawingMode('stylus')">
34
+ <button class="icon-btn" :class="{ shallow: drawingMode !== 'stylus' }" @click="setDrawingMode('stylus')">
35
35
  <carbon:pen />
36
36
  </button>
37
- <button class="icon-btn" :class="{ shallow: drawingMode != 'line' }" @click="setDrawingMode('line')">
37
+ <button class="icon-btn" :class="{ shallow: drawingMode !== 'line' }" @click="setDrawingMode('line')">
38
38
  <svg width="1em" height="1em" class="-mt-0.5" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
39
39
  <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" />
40
40
  </svg>
41
41
  </button>
42
- <button class="icon-btn" :class="{ shallow: drawingMode != 'arrow' }" @click="setDrawingMode('arrow')">
42
+ <button class="icon-btn" :class="{ shallow: drawingMode !== 'arrow' }" @click="setDrawingMode('arrow')">
43
43
  <carbon:arrow-up-right />
44
44
  </button>
45
- <button class="icon-btn" :class="{ shallow: drawingMode != 'ellipse' }" @click="setDrawingMode('ellipse')">
45
+ <button class="icon-btn" :class="{ shallow: drawingMode !== 'ellipse' }" @click="setDrawingMode('ellipse')">
46
46
  <carbon:radio-button />
47
47
  </button>
48
- <button class="icon-btn" :class="{ shallow: drawingMode != 'rectangle' }" @click="setDrawingMode('rectangle')">
48
+ <button class="icon-btn" :class="{ shallow: drawingMode !== 'rectangle' }" @click="setDrawingMode('rectangle')">
49
49
  <carbon:checkbox />
50
50
  </button>
51
51
  <!-- TODO: not sure why it's not working! -->
@@ -65,7 +65,7 @@ function setBrushColor(color: typeof brush.color) {
65
65
  <div
66
66
  class="w-6 h-6 transition-all transform border border-gray-400/50"
67
67
  :class="brush.color !== color ? 'rounded-1/2 scale-85' : 'rounded-md'"
68
- :style="drawingEnabled? { background: color } : { borderColor: color }"
68
+ :style="drawingEnabled ? { background: color } : { borderColor: color }"
69
69
  />
70
70
  </button>
71
71
 
@@ -127,13 +127,13 @@ throttledWatch(
127
127
  <template>
128
128
  <div
129
129
  class="fixed h-full top-0 bottom-0 w-10px bg-gray-400 select-none opacity-0 hover:opacity-10 z-100"
130
- :class="{'!opacity-30': handlerDown}"
131
- :style="{right: `${editorWidth - 5}px`, cursor: 'col-resize'}"
130
+ :class="{ '!opacity-30': handlerDown }"
131
+ :style="{ right: `${editorWidth - 5}px`, cursor: 'col-resize' }"
132
132
  @pointerdown="onHandlerDown"
133
133
  />
134
134
  <div
135
135
  class="shadow bg-main p-4 grid grid-rows-[max-content,1fr] h-full overflow-hidden border-l border-gray-400 border-opacity-20"
136
- :style="{width: `${editorWidth}px`}"
136
+ :style="{ width: `${editorWidth}px` }"
137
137
  >
138
138
  <div class="flex pb-2 text-xl -mt-1">
139
139
  <div class="mr-4 rounded flex">
@@ -1,16 +1,29 @@
1
1
  <script setup lang="ts">
2
2
  import { computed, nextTick, ref, watch } from 'vue'
3
- import { go, total } from '../logic/nav'
3
+ import { go, rawRoutes, total } from '../logic/nav'
4
4
  import { showGotoDialog } from '../state'
5
5
 
6
6
  const input = ref<HTMLInputElement>()
7
7
  const text = ref('')
8
- const num = computed(() => +text.value)
9
- const valid = computed(() => !isNaN(num.value) && num.value > 0 && num.value <= total.value)
8
+
9
+ const valid = computed(() => {
10
+ if (text.value.startsWith('/')) {
11
+ return !!rawRoutes.find(r => r.path === text.value.substring(1))
12
+ }
13
+ else {
14
+ const num = +text.value
15
+ return !isNaN(num) && num > 0 && num <= total.value
16
+ }
17
+ })
10
18
 
11
19
  function goTo() {
12
- if (valid.value)
13
- go(num.value)
20
+ if (valid.value) {
21
+ if (text.value.startsWith('/'))
22
+ go(text.value.substring(1))
23
+
24
+ else
25
+ go(+text.value)
26
+ }
14
27
  close()
15
28
  }
16
29
 
@@ -18,7 +31,7 @@ function close() {
18
31
  showGotoDialog.value = false
19
32
  }
20
33
 
21
- watch(showGotoDialog, async(show) => {
34
+ watch(showGotoDialog, async (show) => {
22
35
  if (show) {
23
36
  await nextTick()
24
37
  text.value = ''
@@ -31,8 +44,8 @@ watch(showGotoDialog, async(show) => {
31
44
 
32
45
  // remove the g character coming from the key that triggered showGotoDialog (e.g. in Firefox)
33
46
  watch(text, (t) => {
34
- if (t.match(/^[^0-9]/))
35
- text.value = text.value.substr(1)
47
+ if (t.match(/^[^0-9/]/))
48
+ text.value = text.value.substring(1)
36
49
  })
37
50
  </script>
38
51
 
@@ -4,13 +4,13 @@ import { computed } from 'vue'
4
4
  import { configs } from '../env'
5
5
  import Modal from './Modal.vue'
6
6
 
7
- const emit = defineEmits<{ (name: 'modelValue', v: boolean): void }>()
8
7
  const props = defineProps({
9
8
  modelValue: {
10
9
  default: false,
11
10
  },
12
11
  })
13
12
 
13
+ const emit = defineEmits<{ (name: 'modelValue', v: boolean): void }>()
14
14
  const value = useVModel(props, 'modelValue', emit)
15
15
  const hasInfo = computed(() => typeof configs.info === 'string')
16
16
  </script>
@@ -2,9 +2,6 @@
2
2
  import { onClickOutside, useVModel } from '@vueuse/core'
3
3
  import { ref } from 'vue'
4
4
 
5
- const emit = defineEmits<{
6
- (e: any): void
7
- }>()
8
5
  const props = defineProps({
9
6
  modelValue: {
10
7
  default: false,
@@ -14,6 +11,9 @@ const props = defineProps({
14
11
  },
15
12
  })
16
13
 
14
+ const emit = defineEmits<{
15
+ (e: any): void
16
+ }>()
17
17
  const value = useVModel(props, 'modelValue', emit, { passive: true })
18
18
  const el = ref<HTMLDivElement>()
19
19
 
@@ -1,7 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import { useVModel } from '@vueuse/core'
3
3
 
4
- const emit = defineEmits<{ (name: 'modelValue', v: boolean): void }>()
5
4
  const props = defineProps({
6
5
  modelValue: {
7
6
  default: false,
@@ -11,6 +10,7 @@ const props = defineProps({
11
10
  },
12
11
  })
13
12
 
13
+ const emit = defineEmits<{ (name: 'modelValue', v: boolean): void }>()
14
14
  const value = useVModel(props, 'modelValue', emit)
15
15
 
16
16
  function onClick() {
@@ -35,7 +35,7 @@ const barStyle = computed(() => props.persist
35
35
  )
36
36
 
37
37
  const RecordingControls = shallowRef<any>()
38
- if (__DEV__)
38
+ if (__SLIDEV_FEATURE_RECORD__)
39
39
  import('./RecordingControls.vue').then(v => RecordingControls.value = v.default)
40
40
 
41
41
  const DrawingControls = shallowRef<any>()
@@ -79,7 +79,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
79
79
 
80
80
  <VerticalDivider />
81
81
 
82
- <template v-if="__DEV__ && !isEmbedded">
82
+ <template v-if="!isEmbedded">
83
83
  <template v-if="!isPresenter && !md && RecordingControls">
84
84
  <RecordingControls />
85
85
  <VerticalDivider />
@@ -108,7 +108,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
108
108
  <VerticalDivider />
109
109
  </template>
110
110
 
111
- <template v-if="__DEV__ && !isEmbedded">
111
+ <template v-if="!isEmbedded">
112
112
  <RouterLink v-if="isPresenter" :to="nonPresenterLink" class="icon-btn" title="Play Mode">
113
113
  <carbon:presentation-file />
114
114
  </RouterLink>
@@ -116,7 +116,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
116
116
  <carbon:user-speaker />
117
117
  </RouterLink>
118
118
 
119
- <button v-if="!isPresenter" class="icon-btn <md:hidden" @click="showEditor = !showEditor">
119
+ <button v-if="__DEV__ && !isPresenter" class="icon-btn <md:hidden" @click="showEditor = !showEditor">
120
120
  <carbon:text-annotation-toggle />
121
121
  </button>
122
122
  </template>
@@ -43,7 +43,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
43
43
  <div id="page-root" ref="root" class="grid grid-cols-[1fr,max-content]" :style="themeVars">
44
44
  <SlideContainer
45
45
  class="w-full h-full"
46
- :style="{ background: 'var(--slidev-slide-container-background, black)'}"
46
+ :style="{ background: 'var(--slidev-slide-container-background, black)' }"
47
47
  :width="isPrintMode ? windowSize.width.value : undefined"
48
48
  :scale="slideScale"
49
49
  @pointerdown="onClick"
@@ -56,7 +56,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__)
56
56
  class="absolute bottom-0 left-0 transition duration-300 opacity-0 hover:opacity-100"
57
57
  :class="[
58
58
  persistNav ? 'opacity-100 right-0' : 'opacity-0 p-2',
59
- isDrawing ? 'pointer-events-none': ''
59
+ isDrawing ? 'pointer-events-none' : '',
60
60
  ]"
61
61
  >
62
62
  <NavControls class="m-auto" :persist="persistNav" />
@@ -4,7 +4,8 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
4
4
  import { useMouse, useWindowFocus } from '@vueuse/core'
5
5
  import { clicks, clicksTotal, currentPage, currentRoute, hasNext, nextRoute, total, useSwipeControls } from '../logic/nav'
6
6
  import { showOverview, showPresenterCursor } from '../state'
7
- import { configs, serverState, themeVars } from '../env'
7
+ import { configs, themeVars } from '../env'
8
+ import { sharedState } from '../state/shared'
8
9
  import { registerShortcuts } from '../logic/shortcuts'
9
10
  import { getSlideClass } from '../utils'
10
11
  import { useTimer } from '../logic/utils'
@@ -72,7 +73,7 @@ onMounted(() => {
72
73
  return { x, y }
73
74
  },
74
75
  (pos) => {
75
- serverState.cursor = pos
76
+ sharedState.cursor = pos
76
77
  },
77
78
  )
78
79
  })
@@ -1,15 +1,15 @@
1
1
  <script setup lang="ts">
2
- import { serverState } from '../env'
2
+ import { sharedState } from '../state/shared'
3
3
  </script>
4
4
 
5
5
  <template>
6
6
  <div
7
- v-if="serverState.cursor"
7
+ v-if="sharedState.cursor"
8
8
  class="absolute top-0 left-0 right-0 bottom-0 pointer-events-none text-xl"
9
9
  >
10
10
  <ph:cursor-fill
11
11
  class="absolute"
12
- :style="{ left: `${serverState.cursor.x}%`, top: `${serverState.cursor.y}%` }"
12
+ :style="{ left: `${sharedState.cursor.x}%`, top: `${sharedState.cursor.y}%` }"
13
13
  />
14
14
  </div>
15
15
  </template>
@@ -30,7 +30,7 @@ watchEffect(() => {
30
30
  <div id="page-root" class="grid grid-cols-[1fr,max-content]" :style="themeVars">
31
31
  <PrintContainer
32
32
  class="w-full h-full"
33
- :style="{ background: 'var(--slidev-slide-container-background, black)'}"
33
+ :style="{ background: 'var(--slidev-slide-container-background, black)' }"
34
34
  :width="windowSize.width.value"
35
35
  />
36
36
  </div>
@@ -35,6 +35,7 @@ if (__SLIDEV_FEATURE_DRAWINGS__ || __SLIDEV_FEATURE_DRAWINGS_PERSIST__)
35
35
 
36
36
  const clicks = computed(() => props.clicks)
37
37
  const navClicks = useNavClicks(clicks, props.nav.currentRoute, props.nav.currentPage)
38
+ const id = computed(() => `${props.route.path.toString().padStart(3, '0')}-${(clicks.value + 1).toString().padStart(2, '0')}`)
38
39
 
39
40
  provide(injectionSlidevContext, reactive({
40
41
  nav: { ...props.nav, ...navClicks },
@@ -44,7 +45,7 @@ provide(injectionSlidevContext, reactive({
44
45
  </script>
45
46
 
46
47
  <template>
47
- <div :id="route.path" class="slide-container" :style="style">
48
+ <div :id="id" class="slide-container" :style="style">
48
49
  <GlobalBottom />
49
50
 
50
51
  <SlideWrapper
@@ -56,9 +57,9 @@ provide(injectionSlidevContext, reactive({
56
57
  />
57
58
  <template
58
59
  v-if="
59
- (__SLIDEV_FEATURE_DRAWINGS__ ||
60
- __SLIDEV_FEATURE_DRAWINGS_PERSIST__) &&
61
- DrawingPreview
60
+ (__SLIDEV_FEATURE_DRAWINGS__
61
+ || __SLIDEV_FEATURE_DRAWINGS_PERSIST__)
62
+ && DrawingPreview
62
63
  "
63
64
  >
64
65
  <DrawingPreview :page="+route.path" />
@@ -24,7 +24,7 @@ function toggleRecording() {
24
24
  <button
25
25
  v-if="currentCamera !== 'none'"
26
26
  class="icon-btn <md:hidden"
27
- :class="{'text-green-500': Boolean(showAvatar && streamCamera)}"
27
+ :class="{ 'text-green-500': Boolean(showAvatar && streamCamera) }"
28
28
  title="Show camera view"
29
29
  @click="toggleAvatar"
30
30
  >
@@ -33,7 +33,7 @@ function toggleRecording() {
33
33
 
34
34
  <button
35
35
  class="icon-btn"
36
- :class="{'text-red-500': recording}"
36
+ :class="{ 'text-red-500': recording }"
37
37
  title="Recording"
38
38
  @click="toggleRecording"
39
39
  >
@@ -5,15 +5,15 @@ import { getFilename, mimeType, recordCamera, recorder, recordingName } from '..
5
5
  import Modal from './Modal.vue'
6
6
  import DevicesList from './DevicesList.vue'
7
7
 
8
- const emit = defineEmits<{
9
- (e: any): void
10
- }>()
11
8
  const props = defineProps({
12
9
  modelValue: {
13
10
  default: false,
14
11
  },
15
12
  })
16
13
 
14
+ const emit = defineEmits<{
15
+ (e: any): void
16
+ }>()
17
17
  const value = useVModel(props, 'modelValue', emit)
18
18
 
19
19
  const { startRecording } = recorder
@@ -3,9 +3,6 @@ import { useVModel } from '@vueuse/core'
3
3
  import type { PropType } from 'vue'
4
4
  import type { SelectionItem } from './types'
5
5
 
6
- const emit = defineEmits<{
7
- (e: any): void
8
- }>()
9
6
  const props = defineProps({
10
7
  modelValue: {
11
8
  type: [Object, String, Number] as PropType<any>,
@@ -18,6 +15,9 @@ const props = defineProps({
18
15
  },
19
16
  })
20
17
 
18
+ const emit = defineEmits<{
19
+ (e: any): void
20
+ }>()
21
21
  const value = useVModel(props, 'modelValue', emit, { passive: true })
22
22
  </script>
23
23
 
@@ -8,9 +8,9 @@ import SlideContainer from './SlideContainer.vue'
8
8
  import SlideWrapper from './SlideWrapper'
9
9
  import DrawingPreview from './DrawingPreview.vue'
10
10
 
11
- const emit = defineEmits([])
12
11
  const props = defineProps<{ modelValue: boolean }>()
13
12
 
13
+ const emit = defineEmits([])
14
14
  const value = useVModel(props, 'modelValue', emit)
15
15
 
16
16
  function close() {
package/logic/drawings.ts CHANGED
@@ -1,8 +1,9 @@
1
- import { computed, markRaw, nextTick, reactive, ref, watch, watchEffect } from 'vue'
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
4
  import { toReactive, useStorage } from '@vueuse/core'
5
- import { configs, serverDrawingState as drawingState } from '../env'
5
+ import { drawingState, onPatch, patch } from '../state/drawings'
6
+ import { configs } from '../env'
6
7
  import { currentPage, isPresenter } from './nav'
7
8
 
8
9
  export const brushColors = [
@@ -29,7 +30,9 @@ export const brush = toReactive(useStorage<Brush>('slidev-drawing-brush', {
29
30
  }))
30
31
 
31
32
  const _mode = ref<DrawingMode | 'arrow'>('stylus')
33
+ const syncUp = computed(() => configs.drawings.syncAll || isPresenter.value)
32
34
  let disableDump = false
35
+
33
36
  export const drawingMode = computed({
34
37
  get() {
35
38
  return _mode.value
@@ -56,7 +59,8 @@ export const drauu = markRaw(createDrauu(drauuOptions))
56
59
 
57
60
  export function clearDrauu() {
58
61
  drauu.clear()
59
- drawingState.$patch({ [currentPage.value]: '' })
62
+ if (syncUp.value)
63
+ patch(currentPage.value, '')
60
64
  }
61
65
 
62
66
  export function updateState() {
@@ -80,24 +84,18 @@ drauu.on('changed', () => {
80
84
  if (!disableDump) {
81
85
  const dump = drauu.dump()
82
86
  const key = currentPage.value
83
- if ((drawingState[key] || '') !== dump) {
84
- if (__DEV__)
85
- drawingState.$patch({ [key]: drauu.dump() })
86
- else
87
- drawingState[key] = drauu.dump()
88
- }
87
+ if ((drawingState[key] || '') !== dump && syncUp.value)
88
+ patch(key, drauu.dump())
89
89
  }
90
90
  })
91
91
 
92
- if (__DEV__) {
93
- drawingState.$onPatch((patch) => {
94
- disableDump = true
95
- if (patch[currentPage.value] != null)
96
- drauu.load(patch[currentPage.value] || '')
97
- disableDump = false
98
- updateState()
99
- })
100
- }
92
+ onPatch((state) => {
93
+ disableDump = true
94
+ if (state[currentPage.value] != null)
95
+ drauu.load(state[currentPage.value] || '')
96
+ disableDump = false
97
+ updateState()
98
+ })
101
99
 
102
100
  nextTick(() => {
103
101
  watch(currentPage, () => {
@@ -105,10 +103,6 @@ nextTick(() => {
105
103
  return
106
104
  loadCanvas()
107
105
  }, { immediate: true })
108
-
109
- watchEffect(() => {
110
- drawingState.$syncUp = configs.drawings.syncAll || isPresenter.value
111
- })
112
106
  })
113
107
 
114
108
  drauu.on('start', () => isDrawing.value = true)
package/logic/nav.ts CHANGED
@@ -23,7 +23,7 @@ export { rawRoutes, router }
23
23
  // force update collected elements when the route is fully resolved
24
24
  const routeForceRefresh = ref(0)
25
25
  nextTick(() => {
26
- router.afterEach(async() => {
26
+ router.afterEach(async () => {
27
27
  await nextTick()
28
28
  routeForceRefresh.value += 1
29
29
  })
@@ -113,7 +113,7 @@ export async function prevSlide(lastClicks = true) {
113
113
  router.replace({ query: { ...route.value.query, clicks: clicksTotal.value } })
114
114
  }
115
115
 
116
- export function go(page: number, clicks?: number) {
116
+ export function go(page: number | string, clicks?: number) {
117
117
  return router.push({ path: getPath(page), query: { ...route.value.query, clicks } })
118
118
  }
119
119
 
package/logic/note.ts CHANGED
@@ -4,7 +4,7 @@ import type { Ref } from 'vue'
4
4
  import { computed, ref, unref } from 'vue'
5
5
  import type { SlideInfo, SlideInfoExtended } from '@slidev/types'
6
6
 
7
- export interface UseSlideInfo{
7
+ export interface UseSlideInfo {
8
8
  info: Ref<SlideInfoExtended | undefined>
9
9
  update: (data: Partial<SlideInfo>) => Promise<SlideInfoExtended | void>
10
10
  }
@@ -13,7 +13,7 @@ export function useSlideInfo(id: number | undefined): UseSlideInfo {
13
13
  if (id == null) {
14
14
  return {
15
15
  info: ref() as Ref<SlideInfoExtended | undefined>,
16
- update: async() => {},
16
+ update: async () => {},
17
17
  }
18
18
  }
19
19
  const url = `/@slidev/slide/${id}.json`
@@ -21,7 +21,7 @@ export function useSlideInfo(id: number | undefined): UseSlideInfo {
21
21
 
22
22
  execute()
23
23
 
24
- const update = async(data: Partial<SlideInfo>) => {
24
+ const update = async (data: Partial<SlideInfo>) => {
25
25
  return await fetch(
26
26
  url,
27
27
  {
@@ -35,10 +35,12 @@ export function useSlideInfo(id: number | undefined): UseSlideInfo {
35
35
  ).then(r => r.json())
36
36
  }
37
37
 
38
- import.meta.hot?.on('slidev-update', (payload) => {
39
- if (payload.id === id)
40
- info.value = payload.data
41
- })
38
+ if (__DEV__) {
39
+ import.meta.hot?.on('slidev-update', (payload) => {
40
+ if (payload.id === id)
41
+ info.value = payload.data
42
+ })
43
+ }
42
44
 
43
45
  return {
44
46
  info,
@@ -58,7 +60,7 @@ export function useDynamicSlideInfo(id: MaybeRef<number | undefined>) {
58
60
 
59
61
  return {
60
62
  info: computed(() => get(unref(id)).info.value),
61
- update: async(data: Partial<SlideInfo>, newId?: number) => {
63
+ update: async (data: Partial<SlideInfo>, newId?: number) => {
62
64
  const info = get(newId ?? unref(id))
63
65
  const newData = await info.update(data)
64
66
  if (newData)
@@ -110,18 +110,18 @@ export function useRecording() {
110
110
  video: currentCamera.value === 'none' || recordCamera.value !== true
111
111
  ? false
112
112
  : {
113
- deviceId: currentCamera.value,
114
- },
113
+ deviceId: currentCamera.value,
114
+ },
115
115
  audio: currentMic.value === 'none'
116
116
  ? false
117
117
  : {
118
- deviceId: currentMic.value,
119
- },
118
+ deviceId: currentMic.value,
119
+ },
120
120
  })
121
121
  }
122
122
  }
123
123
 
124
- watch(currentCamera, async(v) => {
124
+ watch(currentCamera, async (v) => {
125
125
  if (v === 'none') {
126
126
  closeStream(streamCamera)
127
127
  }
@@ -8,7 +8,7 @@ import { isDark } from '../logic/dark'
8
8
  import { injectionSlidevContext } from '../constants'
9
9
  import { useContext } from '../composables/useContext'
10
10
 
11
- export type SlidevContextNavKey = 'route' | 'path' | 'total' | 'currentPage' | 'currentPath' | 'currentRoute' | 'currentSlideId' | 'currentLayout' | 'nextRoute'| 'rawTree' | 'treeWithActiveStatuses' | 'tree' | 'downloadPDF' | 'next' | 'nextSlide' | 'openInEditor' | 'prev' | 'prevSlide'
11
+ export type SlidevContextNavKey = 'route' | 'path' | 'total' | 'currentPage' | 'currentPath' | 'currentRoute' | 'currentSlideId' | 'currentLayout' | 'nextRoute' | 'rawTree' | 'treeWithActiveStatuses' | 'tree' | 'downloadPDF' | 'next' | 'nextSlide' | 'openInEditor' | 'prev' | 'prevSlide'
12
12
  export type SlidevContextNavClicksKey = 'clicks' | 'clicksElements' | 'clicksTotal' | 'hasNext' | 'hasPrev'
13
13
 
14
14
  export type SlidevContextNav = Pick<typeof nav, SlidevContextNavKey>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slidev/client",
3
- "version": "0.30.2",
3
+ "version": "0.31.2",
4
4
  "description": "Presentation slides for developers",
5
5
  "homepage": "https://sli.dev",
6
6
  "bugs": "https://github.com/slidevjs/slidev/issues",
@@ -13,32 +13,31 @@
13
13
  "funding": "https://github.com/sponsors/antfu",
14
14
  "dependencies": {
15
15
  "@antfu/utils": "^0.5.1",
16
- "@slidev/parser": "0.30.2",
17
- "@slidev/types": "0.30.2",
18
- "@vueuse/core": "^8.2.5",
19
- "@vueuse/head": "^0.7.5",
16
+ "@slidev/parser": "0.31.2",
17
+ "@slidev/types": "0.31.2",
18
+ "@vueuse/core": "^8.4.2",
19
+ "@vueuse/head": "^0.7.6",
20
20
  "@vueuse/motion": "^2.0.0-beta.18",
21
- "codemirror": "^5.65.2",
21
+ "codemirror": "^5.65.3",
22
22
  "defu": "^6.0.0",
23
23
  "drauu": "^0.3.0",
24
24
  "file-saver": "^2.0.5",
25
25
  "js-base64": "^3.7.2",
26
26
  "js-yaml": "^4.1.0",
27
27
  "katex": "^0.15.3",
28
- "mermaid": "^9.0.0",
28
+ "mermaid": "^9.0.1",
29
29
  "monaco-editor": "^0.33.0",
30
- "nanoid": "^3.3.2",
30
+ "nanoid": "^3.3.4",
31
31
  "prettier": "^2.6.2",
32
32
  "recordrtc": "^5.6.2",
33
33
  "resolve": "^1.22.0",
34
34
  "vite-plugin-windicss": "^1.8.4",
35
- "vue": "^3.2.32",
36
- "vue-router": "^4.0.14",
37
- "vue-starport": "^0.2.4",
35
+ "vue": "^3.2.33",
36
+ "vue-router": "^4.0.15",
37
+ "vue-starport": "^0.2.10",
38
38
  "windicss": "^3.5.1"
39
39
  },
40
40
  "engines": {
41
41
  "node": ">=14.0.0"
42
- },
43
- "readme": "# @slidev/client\n\n[![NPM version](https://img.shields.io/npm/v/@slidev/client?color=3AB9D4&label=)](https://www.npmjs.com/package/@slidev/client)\n\nClient code for [Slidev](https://sli.dev). Shipped with [`@slidev/cli`](https://www.npmjs.com/package/@slidev/cli).\n\n## License\n\nMIT License © 2021 [Anthony Fu](https://github.com/antfu)\n\n"
42
+ }
44
43
  }
package/routes.ts CHANGED
@@ -19,22 +19,17 @@ export const routes: RouteRecordRaw[] = [
19
19
  { name: 'print', path: '/print', component: Print },
20
20
  { path: '', redirect: { path: '/1' } },
21
21
  { path: '/:pathMatch(.*)', redirect: { path: '/1' } },
22
+ {
23
+ name: 'presenter',
24
+ path: '/presenter/:no',
25
+ component: () => import('./internals/Presenter.vue'),
26
+ },
27
+ {
28
+ path: '/presenter',
29
+ redirect: { path: '/presenter/1' },
30
+ },
22
31
  ]
23
32
 
24
- if (import.meta.env.DEV) {
25
- routes.push(
26
- {
27
- name: 'presenter',
28
- path: '/presenter/:no',
29
- component: () => import('./internals/Presenter.vue'),
30
- },
31
- {
32
- path: '/presenter',
33
- redirect: { path: '/presenter/1' },
34
- },
35
- )
36
- }
37
-
38
33
  export const router = createRouter({
39
34
  history: __SLIDEV_HASH_ROUTE__ ? createWebHashHistory(import.meta.env.BASE_URL) : createWebHistory(import.meta.env.BASE_URL),
40
35
  routes,
package/setup/monaco.ts CHANGED
@@ -4,7 +4,7 @@ import { createSingletonPromise } from '@antfu/utils'
4
4
  import type { MonacoSetupReturn } from '@slidev/types'
5
5
  /* __imports__ */
6
6
 
7
- const setup = createSingletonPromise(async() => {
7
+ const setup = createSingletonPromise(async () => {
8
8
  monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
9
9
  ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(),
10
10
  noUnusedLocals: false,
@@ -16,7 +16,7 @@ const setup = createSingletonPromise(async() => {
16
16
 
17
17
  await Promise.all([
18
18
  // load workers
19
- (async() => {
19
+ (async () => {
20
20
  const [
21
21
  { default: EditorWorker },
22
22
  { default: JsonWorker },
package/setup/root.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /* __imports__ */
2
- import { useHead } from '@vueuse/head'
3
2
  import { watch } from 'vue'
3
+ import { useHead } from '@vueuse/head'
4
+ import { configs } from '../env'
5
+ import { initSharedState, onPatch, patch } from '../state/shared'
6
+ import { initDrawingState } from '../state/drawings'
4
7
  import { clicks, currentPage, getPath, isPresenter } from '../logic/nav'
5
8
  import { router } from '../routes'
6
- import { configs, serverState } from '../env'
7
9
 
8
10
  export default function setupRoot() {
9
11
  // @ts-expect-error injected in runtime
@@ -12,36 +14,30 @@ export default function setupRoot() {
12
14
 
13
15
  /* __injections__ */
14
16
 
15
- useHead({
16
- title: configs.titleTemplate.replace('%s', configs.title || 'Slidev'),
17
- })
17
+ const title = configs.titleTemplate.replace('%s', configs.title || 'Slidev')
18
+ useHead({ title })
19
+ initSharedState(`${title} - shared`)
20
+ initDrawingState(`${title} - drawings`)
21
+
22
+ // update shared state
23
+ function updateSharedState() {
24
+ if (isPresenter.value) {
25
+ patch('page', +currentPage.value)
26
+ patch('clicks', clicks.value)
27
+ }
28
+ }
29
+ router.afterEach(updateSharedState)
30
+ watch(clicks, updateSharedState)
18
31
 
19
- function onServerStateChanged() {
20
- if (isPresenter.value)
21
- return
22
- if (+serverState.page !== +currentPage.value || clicks.value !== serverState.clicks) {
32
+ onPatch((state) => {
33
+ if (+state.page !== +currentPage.value || clicks.value !== state.clicks) {
23
34
  router.replace({
24
- path: getPath(serverState.page),
35
+ path: getPath(state.page),
25
36
  query: {
26
37
  ...router.currentRoute.value.query,
27
- clicks: serverState.clicks || 0,
38
+ clicks: state.clicks || 0,
28
39
  },
29
40
  })
30
41
  }
31
- }
32
- function updateServerState() {
33
- if (isPresenter.value) {
34
- serverState.page = +currentPage.value
35
- serverState.clicks = clicks.value
36
- }
37
- }
38
-
39
- // upload state to server
40
- router.afterEach(updateServerState)
41
- watch(clicks, updateServerState)
42
-
43
- // sync with server state
44
- router.isReady().then(() => {
45
- watch(serverState, onServerStateChanged, { deep: true })
46
42
  })
47
43
  }
@@ -0,0 +1,7 @@
1
+ import serverDrawingState from 'server-reactive:drawings?diff'
2
+ import { createSyncState } from './syncState'
3
+
4
+ export type DrawingsState = Record<number, string | undefined>
5
+
6
+ const { init, onPatch, patch, state } = createSyncState<DrawingsState>(serverDrawingState, {}, __SLIDEV_FEATURE_DRAWINGS_PERSIST__)
7
+ export { init as initDrawingState, onPatch, patch, state as drawingState }
@@ -0,0 +1,17 @@
1
+ import serverState from 'server-reactive:nav'
2
+ import { createSyncState } from './syncState'
3
+
4
+ export interface SharedState {
5
+ page: number
6
+ clicks: number
7
+ cursor?: {
8
+ x: number
9
+ y: number
10
+ }
11
+ }
12
+
13
+ const { init, onPatch, patch, state } = createSyncState<SharedState>(serverState, {
14
+ page: 1,
15
+ clicks: 0,
16
+ })
17
+ export { init as initSharedState, onPatch, patch, state as sharedState }
@@ -0,0 +1,67 @@
1
+ import { reactive, toRaw, watch } from 'vue'
2
+
3
+ export function createSyncState<State extends object>(serverState: State, defaultState: State, persist = false) {
4
+ const onPatchCallbacks: ((state: State) => void)[] = []
5
+ let patching = false
6
+ let updating = false
7
+ let patchingTimeout: NodeJS.Timeout
8
+ let updatingTimeout: NodeJS.Timeout
9
+
10
+ const state = __DEV__
11
+ ? reactive<State>(serverState) as State
12
+ : reactive<State>(defaultState) as State
13
+
14
+ function onPatch(fn: (state: State) => void) {
15
+ onPatchCallbacks.push(fn)
16
+ }
17
+
18
+ function patch<K extends keyof State>(key: K, value: State[K]) {
19
+ clearTimeout(patchingTimeout)
20
+ patching = true
21
+ state[key] = value
22
+ patchingTimeout = setTimeout(() => patching = false, 0)
23
+ }
24
+
25
+ function onUpdate(patch: Partial<State>) {
26
+ if (!patching) {
27
+ clearTimeout(updatingTimeout)
28
+ updating = true
29
+ Object.entries(patch).forEach(([key, value]) => {
30
+ state[key as keyof State] = value as State[keyof State]
31
+ })
32
+ updatingTimeout = setTimeout(() => updating = false, 0)
33
+ }
34
+ }
35
+
36
+ function init(channelKey: string) {
37
+ let stateChannel: BroadcastChannel
38
+ if (!__DEV__ && !persist) {
39
+ stateChannel = new BroadcastChannel(channelKey)
40
+ stateChannel.addEventListener('message', (event: MessageEvent<Partial<State>>) => onUpdate(event.data))
41
+ }
42
+ else if (!__DEV__ && persist) {
43
+ window.addEventListener('storage', (event) => {
44
+ if (event && event.key === channelKey && event.newValue)
45
+ onUpdate(JSON.parse(event.newValue) as Partial<State>)
46
+ })
47
+ }
48
+
49
+ function onDrawingStateChanged() {
50
+ if (!persist && stateChannel && !updating)
51
+ stateChannel.postMessage(toRaw(state))
52
+ else if (persist && !updating)
53
+ window.localStorage.setItem(channelKey, JSON.stringify(state))
54
+ if (!patching)
55
+ onPatchCallbacks.forEach((fn: (state: State) => void) => fn(state))
56
+ }
57
+
58
+ watch(state, onDrawingStateChanged, { deep: true })
59
+ if (!__DEV__ && persist) {
60
+ const serialzedState = window.localStorage.getItem(channelKey)
61
+ if (serialzedState)
62
+ onUpdate(JSON.parse(serialzedState) as Partial<State>)
63
+ }
64
+ }
65
+
66
+ return { init, onPatch, patch, state }
67
+ }